W
Școala de WordPress / HTML & CSS Fundamentals
de la host-profesional.com
1 / 76
Modulul 0 · Lecția 1 6 min citire

Ce înseamnă frontend

Deschizi un site. Vezi un header, niște butoane, un grid cu produse, un formular care-ți cere email-ul. Apeși ceva, pagina reacționează. Redimensionezi fereastra, totul se rearanjează frumos.

Cineva a construit toate astea. Fiecare pixel pe care-l vezi, fiecare click care răspunde, fiecare animație care curge lin — ăsta e frontend development. Iar odată ce înțelegi cum funcționează, web-ul încetează să mai pară magic și începe să pară ceva ce poți modela.

Ce face de fapt un frontend developer

Un frontend developer construiește partea din site sau aplicație pe care oamenii o văd și cu care interacționează. Atât. Definiția sună simplu, dar munca din spate se împarte în trei straturi pe care le vei învăța în restul cursului:

  • HTML — structura și conținutul. Titluri, paragrafe, imagini, link-uri, formulare. Gândește-te la el ca la scheletul paginii.
  • CSS — aspectul. Culori, spațiere, fonturi, layout, animații. Pielea și hainele.
  • JavaScript — comportamentul. Ce se întâmplă când apeși, tastezi, scrollezi, trimiți. Sistemul nervos.

În acest curs vei stăpâni primele două. JavaScript vine mai târziu, într-un curs dedicat — merită propriul lui focus.

Exemplu explicat
Același buton, cu fiecare strat adăugat
<!-- Doar HTML: structură -->
<button>Cumpără acum</button>
<!-- HTML + CSS: aspect -->
<button class="cta">Cumpără acum</button>

<style>
  .cta {
    background: #4a7c59;
    color: white;
    padding: 10px 22px;
    border-radius: 7px;
  }
</style>
<!-- HTML + CSS + JS: comportament -->
<button class="cta"
  onclick="alert('Comandă plasată!')">
  Cumpără acum
</button>
Fără CSS, butonul funcționează dar arată ca în 1995. Fără JavaScript, arată super dar nu se întâmplă nimic când apeși. Toate trei împreună fac o interfață modernă.

Frontend vs backend vs full-stack

Oamenii aruncă cuvintele astea în conversație de parcă toată lumea știe ce înseamnă. Versiunea onestă:

Frontend e tot ce rulează în browser-ul utilizatorului. Scrii cod, browser-ul afișează ceva. Ciclu rapid de feedback, muncă foarte vizuală.

Backend e tot ce rulează pe server, invizibil pentru utilizator. Baze de date, API-uri, autentificare, procesare plăți. Când trimiți un formular, backend-ul e cel care primește, salvează și răspunde.

Full-stack înseamnă un dezvoltator care face ambele. Util pentru echipe mici și freelance, dar majoritatea job-urilor sunt specializate pe o parte.

Dacă îți place să vezi imediat rezultatul vizual al muncii tale, frontend e de obicei mai potrivit. Dacă-ți plac puzzle-urile logice și datele, backend ți-ar putea plăcea mai mult. Nu trebuie să alegi pentru totdeauna — mulți developeri încep cu unul și plutesc spre celălalt.

Idei greșite des întâlnite
  • Frontend nu e „doar design". Un designer decide cum ar trebui să arate ceva. Un frontend developer face să funcționeze efectiv pe mii de device-uri, browsere și dimensiuni de ecran.
  • Frontend nu e „ușor" față de backend. E diferit. Să faci un layout care arată bine pe un telefon de 320px și pe un monitor 4K cu același cod e un tip de dificultate propriu.
  • Nu trebuie să fii designer ca să faci frontend. Îți trebuie gust și atenție la detalii. Pe astea ți le poți construi.

Ce vei putea face după acest curs

La finalul cursului HTML & CSS Fundamentals, vei putea:

  1. Construi orice pagină web statică de la zero
  2. Să arate profesional fără să copiezi template-uri
  3. Să funcționeze pe orice dimensiune de ecran, de la telefon la desktop
  4. Să fie accesibilă pentru persoane care folosesc cititoare de ecran sau navighează din tastatură
  5. Să o publici pe internet, ca oricine din lume s-o poată vizita

E suficient să poți prelua muncă reală de freelance, să-ți construiești un portofoliu sau să aplici pentru un rol junior frontend. Când adaugi și JavaScript (următorul curs), treci pragul către aplicații reale.

Prima ta vedere din ce urmează Live

Nu-ți face griji că nu înțelegi sintaxa încă — pentru asta e Modulul 1. Important e să simți ciclul: tastezi, browser-ul afișează, instant.

Cum să profiți la maximum de acest curs

Trei reguli care separă oamenii care termină de cei care renunță:

Scrie cod odată cu lecția, mereu. A privi pe altcineva scriind cod e ca a privi pe altcineva făcând exerciții fizice. Deschide playground-ul, tastează tu însuți, sparge, repară. Acolo se întâmplă învățarea de fapt.

Nu sări exercițiile. Sunt scurte intenționat. Fiecare consolidează conceptul din lecția anterioară. Să le sari înseamnă că următoarea lecție se construiește pe nisip.

Construiește proiectele pe bune. La finalul câtorva module vei construi ceva complet. Nu doar urmări — fă schimbări mici, adaugă detalii proprii, pune-ți numele în footer. Așa le vei ține minte.

Verificare rapidă
Care dintre acestea NU e responsabilitatea HTML-ului?
Recapitulare
  • Frontend development construiește ce văd și cu ce interacționează utilizatorii în browser
  • Cele trei straturi sunt HTML (structură), CSS (stil), JavaScript (comportament)
  • Acest curs acoperă HTML și CSS în profunzime — JavaScript primește propriul curs mai târziu
  • Calea înainte e: scrii cod, spargi lucruri, construiești proiecte. Nu doar citești.

Lecția următoare deschidem capota web-ului — ce se întâmplă de fapt când tastezi un URL și apeși enter. E scurtă, dar va face totul după ea să se așeze la locul lui.

Modulul 0 · Lecția 2 7 min citire

Cum funcționează web-ul

Tastezi un URL, apeși enter, apare o pagină. Pare instantaneu. Pare evident.

În spatele acelei jumătăți de secundă se întâmplă un întreg dans între laptop-ul tău și un calculator undeva în lume. Odată ce vezi coregrafia, vei scrie cod mai bun — pentru că vei ști ce face browser-ul de fapt cu el.

Versiunea într-o propoziție

Când vizitezi un site, browser-ul tău cere de la un calculator numit server niște fișiere, serverul le trimite înapoi, iar browser-ul le asamblează în pagina pe care o vezi.

Atât. Restul e detaliu. Dar detaliile contează, așa că mergem un strat mai adânc.

Călătoria unei cereri

Iată ce se întâmplă când tastezi example.com și apeși enter:

  1. Browser-ul întreabă: „Care calculator e example.com?" — această căutare se numește DNS. E ca o agendă telefonică ce traduce nume în numere (adrese IP).
  2. Browser-ul deschide o conexiune cu acel calculator (serverul) și spune: „Trimite-mi pagina pentru example.com."
  3. Serverul se uită la cerere și decide ce să trimită înapoi — de obicei un fișier HTML, plus instrucțiuni despre ce alte fișiere va mai avea nevoie browser-ul.
  4. Browser-ul citește HTML-ul și observă că pomenește alte fișiere: un stylesheet, câteva imagini, poate un fișier JavaScript. Le cere și pe ele.
  5. Odată ce toate au sosit, browser-ul asamblează totul în pagina pe care o vezi.

Toate astea se întâmplă în câteva sute de milisecunde pe o conexiune decentă.

Exemplu explicat
Ce cere browser-ul de fapt
GET /index.html
Host: example.com
Fiecare cerere e un mic mesaj text. Iar serverul răspunde cu ceva de genul:
200 OK
Content-Type: text/html

<!DOCTYPE html>
<html>
  <body>
    <h1>Salut</h1>
  </body>
</html>
200 OK înseamnă „am găsit, uite". Dacă pagina nu există, ai primi 404 Not Found — de acolo vine celebrul cod de eroare.

Cele trei fișiere pe care browser-ul le iubește

Orice pagină web, de la cea mai simplă la Gmail, se reduce la trei tipuri de fișiere pe care browser-ul le înțelege nativ:

  • .html — documentul în sine. Structură și conținut.
  • .css — stylesheet-uri. Cum ar trebui să arate documentul.
  • .js — JavaScript. Cum ar trebui să se comporte documentul.

Tehnic, poți avea o pagină doar cu HTML. Poți adăuga CSS să arate frumos. Poți adăuga JavaScript s-o faci interactivă. Browser-ul gestionează toate trei fără să aibă nevoie de vreun plugin sau software suplimentar.

Orice altceva — React, Vue, Tailwind, TypeScript, build tools — se compilează în final la aceste trei tipuri de fișiere. De aceea învățarea HTML-ului și CSS-ului în profunzime nu e niciodată timp pierdut. Înveți runtime-ul, nu un trend.

O greșeală comună de model mental

Oamenii noi în web development cred adesea că „site-ul" e pe calculatorul lor după ce-l vizitează. Nu chiar. Ce ai e o copie temporară pe care browser-ul a descărcat-o ca să-ți afișeze pagina. Site-ul propriu-zis trăiește pe server. Fiecare vizitator primește propria copie proaspătă livrată la cerere.

De aceea un site poate arăta diferit pentru oameni diferiți (țară diferită, logat vs nelogat, oră diferită din zi) — serverul decide ce să trimită în funcție de cine întreabă.

Unde va trăi codul tău

Când înveți, codul tău trăiește pe calculatorul tău într-un folder. Deschizi fișierul HTML în browser și vezi rezultatul — nu e nevoie de server încă. Browser-ul citește fișierul direct de pe disc.

Când vrei ca alți oameni să-ți vadă site-ul, uploadezi aceleași fișiere pe un server undeva. Asta poate fi:

  • Un cont de shared hosting (ca cele pe care probabil le cunoști de la WordPress)
  • Un serviciu de static hosting precum Netlify, Vercel sau GitHub Pages (de obicei gratuit)
  • Un VPS sau server cloud pe care-l închiriezi la ora

Pe durata acestui curs, vei lucra local pe calculatorul tău. Deployment-ul îl acoperim în modulul final.

HTTP, HTTPS și de ce vezi un lacăt

Protocolul pe care browserele îl folosesc să vorbească cu serverele se numește HTTP — HyperText Transfer Protocol. Când vezi https:// în bara de URL, e HTTP plus un strat extra de criptare. Iconița mică cu lacăt înseamnă că conexiunea e securizată — nimeni între tine și server nu poate citi ce curge înainte și înapoi.

În zilele noastre, orice site serios folosește HTTPS. Browserele avertizează activ utilizatorii când un site nu o face. Pentru site-urile pe care le construiești, HTTPS e de obicei automat — majoritatea serviciilor de hosting îl configurează gratuit.

Verificare rapidă
Când vizitezi un site, unde trăiește efectiv codul HTML al paginii?
Recapitulare
  • A vizita un site e o conversație: browser-ul cere, serverul răspunde
  • Fiecare pagină e făcută din HTML (structură), CSS (stil) și JavaScript (comportament)
  • DNS traduce nume de domenii în adrese IP, ca browserele să știe unde să meargă
  • HTTPS e HTTP cu criptare — standardul pentru site-urile moderne
  • Codul tău va trăi în fișiere pe calculatorul tău cât înveți, apoi pe un server când publici
Modulul 0 · Lecția 3 8 min citire

Instalare VS Code & setup

Orice frontend developer profesionist folosește aceeași trusă de bază: un editor de cod, un browser și câteva obiceiuri. Uneltele sunt gratuite. Obiceiurile sunt simple. Zece minute de setup acum îți salvează ore de frustrare mai târziu.

Hai să le instalăm.

De ce ai nevoie

Pentru acest curs, și pentru orice muncă de frontend în 2026, iată trusa minimă:

  • Un editor de cod — vom folosi VS Code
  • Un browser modern — Chrome, Firefox sau Edge (nu Internet Explorer, și nu default Safari dacă ești pe Mac — deși Safari merge)
  • Un folder undeva pe calculatorul tău unde-ți vei pune munca

Atât. Fără instalări de runtime-uri ciudate, fără linie de comandă, fără Node.js încă. Tot ce e în Modulele 1-9 rulează în browser-ul tău, citind fișiere de pe disc.

Pasul 1: Instalează VS Code

VS Code (Visual Studio Code) e un editor de cod gratuit făcut de Microsoft. E ce folosesc majoritatea frontend developerilor — inclusiv eu, în fiecare zi.

Mergi pe code.visualstudio.com și descarcă installer-ul pentru sistemul tău. Instalarea e simplă — apeși next de câteva ori și gata.

Nu confunda cele două

VS Code (ce vrei) e un editor de cod lightweight.
Visual Studio (ce nu vrei) e un IDE masiv pentru C#/.NET.
Aceeași companie, produse foarte diferite. Asigură-te că pagina spune „Visual Studio Code".

Pasul 2: Instalează două extensii

VS Code are mii de extensii. De majoritatea nu ai nevoie. De astea două, da:

Live Server

Când construiești o pagină web, ai nevoie să vezi rezultatul. Live Server pornește un mic server web local care-ți afișează pagina în browser și o reîmprospătează automat ori de câte ori salvezi un fișier. Salvezi, browser-ul se actualizează, vezi schimbarea — ăsta e ciclul pe care-l vrei.

Cum se instalează: Apasă iconița de extensii din partea stângă a VS Code (arată ca patru pătrate), caută „Live Server" de Ritwick Dey, apasă Install.

Prettier

Prettier îți formatează codul consistent. Indentare, ghilimele, spațiere — toate aplicate automat când salvezi. Vei înceta să te gândești la formatare și te vei concentra pe ce face codul.

Cum se instalează: Același panou de extensii, caută „Prettier - Code formatter", instalează.

După instalare, activează format-on-save: deschide setările (Cmd/Ctrl + virgulă), caută „format on save", bifează caseta.

Ăsta e tot setup-ul de extensii. N-ai nevoie de Emmet (e built-in), n-ai nevoie de syntax highlighting HTML/CSS (built-in), n-ai nevoie de auto-close tags (built-in).

Pasul 3: Creează folderul de proiect

Undeva pe calculatorul tău — poate Documents/curs-web/ — fă un folder nou. Aici vor trăi toate fișierele din acest curs.

În interiorul acelui folder, creează trei lucruri:

curs-web/
├── lectia-01/
│   └── index.html
├── lectia-02/
└── ...

Fiecare lecție primește propriul subfolder cu un fișier index.html. De ce index.html specific? Pentru că serverele web folosesc index.html ca pagina implicită când vizitezi un folder — e o convenție care va continua să conteze toată cariera ta.

Exemplu explicat
Un fișier HTML minimal să-ți testezi setup-ul
<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <title>Prima mea pagină</title>
</head>
<body>
  <h1>Funcționează!</h1>
  <p>Oficial construiesc pentru web.</p>
</body>
</html>
Deschide VS Code. Deschide folderul lectia-01 (File → Open Folder). În index.html, lipește codul de mai sus.

Salvează fișierul. Click dreapta în editor → „Open with Live Server". Browser-ul se deschide, vezi un titlu care spune „Funcționează!" și un paragraf dedesubt.

Felicitări. Asta e o pagină web pe care ai construit-o tu.

Câteva obiceiuri VS Code de construit de acum

Shortcut-urile de tastatură salvează ore. Trei de învățat primele:

  • Cmd/Ctrl + S — salvează (declanșează Prettier + reîmprospătare Live Server)
  • Cmd/Ctrl + / — comentează/decomentează linia curentă
  • Cmd/Ctrl + D — selectează următoarea apariție a aceluiași cuvânt (apoi tastează să înlocuiești tot deodată)

Păstrează-ți fișierele organizate. O lecție = un folder. Nu arunca totul într-un mega-folder — vei regreta prin Modulul 4.

Ține terminalul închis dacă nu-ți trebuie. În acest curs nu va fi nevoie, până la final. Dacă vezi tutoriale rulând npm install sau npx — alea sunt lucruri mai avansate, ajungem la ele mai târziu.

Pasul 4: Înțelege DevTools-ul browser-ului

Apasă F12 în orice browser (sau click dreapta → Inspect). Se deschide un panou cu tab-uri: Elements, Console, Network, Sources. Astea sunt parbrizul tău spre ce face browser-ul de fapt cu codul tău.

Nu trebuie să le stăpânești acum. Doar să știi că există. Pe parcursul cursului, o să-ți arăt feature-uri specifice DevTools la momentul potrivit.

Recapitulare
  • VS Code e editorul tău, Chrome/Firefox/Edge sunt browserele
  • Instalează doar două extensii: Live Server (pentru reîmprospătare live) și Prettier (pentru formatare)
  • Fiecare lecție primește propriul folder cu un fișier index.html înăuntru
  • Familiarizează-te cu DevTools — apasă F12, explorează, nu te stresa să înțelegi tot
  • Shortcut-urile de tastatură nu-s opționale dacă vrei să lucrezi rapid
Modulul 0 · Lecția 4 5 min citire

Cum să folosești cursul

Majoritatea oamenilor care încep un curs de web development nu-l termină. Nu pentru că e prea greu cursul, ci pentru că-l tratează ca pe o serie Netflix — urmărești, dai din cap, mergi mai departe.

Lecția asta e cea mai simplă din curs. Dar dacă îți iei cinci minute să o citești cu adevărat și să fii de acord, vei fi în minoritatea mică ce efectiv învață să construiască site-uri.

Cum arată fiecare lecție

Din lecția următoare în colo, fiecare lecție din curs urmează aceeași formă:

  1. Hook — de ce contează subiectul, într-un paragraf scurt
  2. Explicație — conceptul, explicat direct
  3. Exemplu — cod pe care-l poți citi, cu rezultatul alături
  4. Playground — cod pe care-l poți edita, rulând live în browser
  5. Greșeli comune — ce greșesc începătorii în acel punct
  6. Exercițiu — o mică provocare cu verificare automată
  7. Recapitulare — trei puncte cu care să rămâi

Lecțiile sunt de 5-15 minute fiecare. Scurte intenționat. Nu trebuie să citești trei lecții la rând — creierul tău nu va ține pasul. Fă una, ia o pauză, revino.

Când folosești playground-ul vs editorul tău

Playground-ul (editorul interactiv integrat în fiecare lecție) e unde experimentezi cu exact conceptul care se predă. Ajustezi valori, spargi lucruri, vezi ce se întâmplă. E low-stakes.

Editorul tău propriu (VS Code cu Live Server, din lecția anterioară) e unde construiești proiecte reale. La finalul majorității modulelor, îți voi cere să construiești ceva de la zero — nu în playground, ci în editorul tău, în folderul tău, salvând pe calculatorul tău.

De ce ambele? Pentru că playground-ul te învață conceptele rapid, dar proiectele reale te învață workflow — salvarea fișierelor, organizarea folderelor, debug când ceva merge prost. Ai nevoie de ambele abilități. Playground-ul e roțile ajutătoare; editorul tău e bicicleta.

Cea mai mare greșeală pe care o văd la începători

Citesc fără să tasteze.

Se simte eficient să parcurgi lecțiile, să dai din cap și să-ți spui „da, am înțeles". N-ai înțeles. Nu cu adevărat. Creierul învață codul la fel cum învață să meargă pe bicicletă — făcând, căzând, ajustând, făcând din nou.

De fiecare dată când vezi un exemplu de cod, retastează-l în playground sau în editorul tău. Nu da copy-paste. Actul de a tasta construiește memoria musculară și te forțează să observi detalii pe care altfel le-ai sări.

Cât va dura asta

Cursul are cam 80 de lecții, cu o medie de 10 minute fiecare. Asta e ~13 ore de citit. Dar a învăța ≠ a citi — dacă faci și exercițiile și proiectele (ceea ce ar trebui), socotește 30-40 de ore totale să parcurgi tot.

Ce înseamnă asta în timp calendaristic? Total la tine:

  • Intensiv — 2 ore pe zi, 3-4 săptămâni
  • Seri și weekend-uri — 5-6 ore pe săptămână, ~2 luni
  • Relaxat — o oră când ai chef, 3-4 luni

Nu e vreun premiu pentru terminat rapid. E premiu mare pentru terminat, orice ritm.

Cele trei obiceiuri care contează

Tot ce ține de învățarea web development se reduce la trei obiceiuri. Dacă le construiești, vei termina cursul. Dacă nu, niciun conținut grozav nu te va duce mai departe.

1. Scrie cod odată cu lecția

Fiecare exemplu pe care-l vezi, tastează-l tu. Fiecare playground, chiar deschide-l și joacă-te. Raportul între „citit" și „tastat" ar trebui să fie în jur de 50/50, nu 90/10.

2. Termină ce începi

Fiecare lecție se încheie cu un exercițiu mic. Fă-l înainte să treci la următoarea. Nu „revin eu la ăsta" — nu revii. Exercițiul e unde lecția se așază de fapt. Să-l sari înseamnă că lecția următoare se construiește pe nisip.

3. Construiește proiectele pe bune

La finalul fiecărui grup de module e un proiect. Deschide VS Code, fă un folder nou, construiește-l efectiv. Nu-l urma în silă — fă schimbări mici, adaugă detalii proprii. Pune-l pe calculator, fii mândru de el. Proiectele alea devin portofoliul tău la finalul cursului.

Când te blochezi

Blocat e normal. Blocat e unde se întâmplă învățarea. Iată ordinea în care să încerci lucruri:

  1. Recitește lecția. Adesea răspunsul e cu trei paragrafe mai sus.
  2. Verifică codul tău caracter cu caracter. O greșeală într-un tag sau un punct-virgulă lipsă cauzează 90% din problemele începătorilor. Browser-ul e foarte pretențios.
  3. Caută eroarea pe Google. Fiecare eroare pe care o vei întâlni a fost întâlnită de o mie de oameni înaintea ta. Probabil e un răspuns la primul rezultat.
  4. Întreabă. Răspund la mesaje directe, de obicei în 24 de ore. E și un canal de comunitate (link în dashboard-ul tău).

Ce nu ar trebui să faci: să sari peste problemă și să speri că lecția următoare o rezolvă. Nu o rezolvă.

Recapitulare
  • Fiecare lecție are aceeași formă: hook, explicație, exemplu, playground, exercițiu, recapitulare
  • Folosește playground-ul pentru concepte, editorul tău pentru proiecte reale
  • Socotește 30-40 de ore totale — împarte-le pe câte săptămâni ți se potrivesc
  • Trei obiceiuri contează: scrii cod odată cu lecția, termini exercițiile, construiești proiectele pe bune
  • Când te blochezi, recitește, verifică codul, Google, întrebi — în ordinea asta
Modulul 1 · Lecția 1 10 min citire

Primul document HTML

Fiecare pagină web de pe internet — de la un blog personal la Amazon — începe cu același schelet. Odată ce-l știi, poți să te uiți la sursa oricărei pagini și să înțelegi ce se întâmplă. Poți și să scrii una de la zero în 30 de secunde.

Acest schelet e despre ce e lecția.

Anatomia unui document HTML

Iată cel mai simplu document HTML valid:

<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <title>Pagina mea</title>
</head>
<body>
  <h1>Salut, lume</h1>
</body>
</html>

Șase linii de schelet, o linie de conținut propriu-zis. Hai să parcurgem fiecare bucată.

Declarația <!DOCTYPE html>

Prima linie a fiecărui fișier HTML. Nu e chiar un tag — e o instrucțiune pentru browser: „tratează asta ca HTML modern."

O scrii exact așa cum e afișat. Nu e nimic de personalizat, nimic de reținut, doar că e prima linie. Mereu.

De ce e acolo

HTML-ul vechi avea multe versiuni cu particularități diferite. DOCTYPE-ul specifica versiunea — și dacă-l uitai, browserele intrau în „quirks mode" și-ți randa pagina ciudat să se potrivească cu bug-uri de la 1999.

Astăzi există un singur HTML (numit adesea HTML5) iar <!DOCTYPE html> e modul universal de a spune „modul standard, te rog". Doar scrie-l.

Elementul <html>

Tot restul trăiește înăuntrul <html>. E containerul cel mai exterior al documentului.

Atributul lang spune browser-ului (și cititoarelor de ecran, și motoarelor de căutare, și uneltelor de traducere) în ce limbă e pagina. Folosește "ro" pentru română, "en" pentru engleză, "fr" pentru franceză, etc. E un cod de două litere bazat pe standardul ISO.

Nu e cosmetic — setând lang corect ajută uneltele de accesibilitate să pronunțe cuvintele corect și ajută browserele să ofere traducere când e nevoie. Setează-l mereu.

Elementul <head>

<head> conține metadate — informații despre pagină care nu apar în pagina propriu-zisă. Gândește-te la el ca la panoul de setări al paginii.

Lucruri comune care trăiesc în <head>:

  • <meta charset="UTF-8"> — spune browser-ului ce codare de caractere să folosească. UTF-8 suportă orice limbă și emoji, deci folosește-l mereu.
  • <title> — textul care apare în tab-ul browser-ului și în rezultatele motoarelor de căutare. Fiecare pagină ar trebui să aibă unul.
  • <meta name="description"> — o descriere scurtă a paginii. Motoarele de căutare o afișează ca snippet sub titlul paginii tale.
  • <link rel="stylesheet" href="..."> — fișiere CSS externe (le vom folosi mult începând cu Modulul 2).
  • <script src="..."> — fișiere JavaScript.

Nimic din astea nu se arată pe pagină. E configurare.

Elementul <body>

<body> conține tot ce utilizatorul vede efectiv. Text, imagini, butoane, formulare, videoclipuri — toate merg în <body>.

În exemplul nostru minimal, body-ul are doar un titlu. Majoritatea paginilor reale au sute de elemente în <body>, organizate în secțiuni.

Exemplu explicat
O pagină puțin mai realistă
<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Despre mine — portofoliul lui Alex</title>
  <meta name="description" content="Frontend developer din București.">
</head>
<body>
  <h1>Salut, sunt Alex</h1>
  <p>Construiesc site-uri pentru afaceri mici.</p>
</body>
</html>
<meta name="viewport"> e crucial pentru mobil. Fără el, telefoanele randează pagina ca și cum ar fi un monitor desktop, zoom-at out. Fiecare pagină modernă include această linie. Scrie-o exact așa.

<title>-ul e descriptiv. „Despre mine — portofoliul lui Alex" e mai bun decât doar „Despre" pentru că apare în rezultatele de căutare și în tab-urile browser-ului unde contextul ajută utilizatorul să găsească ce caută.

Regulile tag-urilor HTML

Trei reguli care se aplică fiecărui tag pe care-l vei scrie vreodată:

1. Majoritatea tag-urilor vin în perechi. Un tag de deschidere ca <p> și unul de închidere ca </p>. Conținutul stă între ele. Tag-ul de închidere are un slash.

2. Unele tag-uri sunt self-closing. Nu au conținut și nu necesită tag de închidere. Exemple: <img>, <br>, <input>, <meta>.

3. Tag-urile se imbrică. Poți pune tag-uri în alte tag-uri. Tag-ul interior trebuie închis înainte ca cel exterior să se închidă.

Construiește prima ta pagină Live

Atribute: configurarea tag-urilor

Tag-urile au adesea atribute — informații suplimentare scrise în tag-ul de deschidere:

<html lang="ro">
<a href="https://example.com">Vezi example</a>
<img src="poza.jpg" alt="O poză cu un câine">

Tiparul e nume="valoare". Valorile merg în ghilimele duble. Mai multe atribute sunt separate prin spații.

Fiecare tag are propriul set de atribute care au sens pentru el. Le vom întâlni pe parcurs. Deocamdată, doar să știi că atunci când vezi nume="valoare" într-un tag, acela e un atribut care configurează comportamentul tag-ului.

Verificare rapidă
Unde trebuie să pui tag-ul <title> într-un document HTML?
Recapitulare
  • Fiecare document HTML începe cu <!DOCTYPE html> și împachetează totul în <html lang="...">
  • <head> ține metadatele (title, charset, viewport, descrieri) — invizibile utilizatorilor
  • <body> ține conținutul vizibil — tot ce vede utilizatorul și cu ce interacționează
  • Tag-urile vin în perechi (cu </tag> de închidere) sau sunt self-closing (<img>, <meta>)
  • Atributele merg în tag-urile de deschidere ca nume="valoare"
Modulul 1 · Lecția 2 9 min citire

Elemente de text

O mare parte din web e doar text. Articole de blog, descrieri de produse, pagini „despre", documentație. Înainte să ajungem la layout-uri fanteziste și interacțiuni, hai să stăpânim tag-urile care tratează cuvintele cum trebuie.

Titluri: <h1> până la <h6>

HTML are șase niveluri de titluri, de la <h1> (cel mai important) la <h6> (cel mai puțin important). Browser-ul afișează nivelurile superioare cu text mai mare implicit, dar adevărata treabă a titlurilor e semantică — îi spun browser-ului, motoarelor de căutare și cititoarelor de ecran care e structura conținutului tău.

<h1>Titlul principal al paginii</h1>
<h2>O secțiune majoră</h2>
<h3>O sub-secțiune din acea secțiune</h3>
<h4>O subdiviziune și mai mică</h4>
<h5>Rar folosit</h5>
<h6>Aproape niciodată folosit</h6>

Gândește-te la titluri ca la un cuprins. <h1> e titlul întregii pagini. <h2>-urile sunt capitolele. <h3>-urile sunt secțiunile din capitole. Și așa mai departe.

Regula unui singur h1

Folosește exact un <h1> per pagină. E titlul principal al paginii.

Alte reguli care contează:

  • Nu sări peste niveluri. Nu sări de la <h2> la <h4> — confuză cititoarele de ecran și uneltele SEO.
  • Nu folosi titluri pentru stilizare. Dacă vrei text mare și bold, folosește CSS. Titlurile sunt pentru structură, nu decorație.
  • Potrivește nivelul titlului cu importanța, nu cu cât de mare vrei să arate textul.

Paragrafe: <p>

Tag-ul de paragraf e exact ce pare: un bloc de text curgător.

<p>Acesta e un paragraf. Poate conține mai multe propoziții. Browser-ul
încadrează liniile automat pe baza lățimii disponibile — nu încerca
să controlezi asta cu line break-uri.</p>

<p>Acesta e un alt paragraf. Paragrafele primesc ceva spațiu vertical
deasupra și dedesubt implicit.</p>

Câteva lucruri de știut:

  • Line break-urile din sursa ta nu contează. Browser-ul colapsează mai multe spații și newline-uri într-un singur spațiu.
  • Paragrafele ar trebui să conțină text curgător, nu layout. Nu pune titluri sau alte paragrafe într-un <p>.
  • Scurt sau lung e ok. Un paragraf de o propoziție e valid.

Formatare inline: <strong>, <em>, <code>, <mark>

În paragrafe, adesea vrei să accentuezi sau să formatezi câteva cuvinte.

<strong> marchează textul ca important. Browserele îl afișează bold implicit. Folosește-l pentru lucruri care contează — avertismente, termeni cheie, accentuare reală.

<em> marchează textul cu accent de tonalitate. Browserele îl afișează italic. Folosește-l cum ai ridica vocea pe un cuvânt când vorbești.

<code> marchează textul ca bucată de cod. Browserele îl afișează cu font monospace. Folosește-l pentru nume de fișiere, nume de funcții, variabile, fragmente scurte.

<mark> evidențiază textul. Browserele îl afișează cu fundal galben.

strong vs b, em vs i

S-ar putea să vezi HTML vechi folosind <b> și <i> în loc de <strong> și <em>. Arată identic în browser. Dar înseamnă lucruri diferite:

  • <strong> / <em> poartă sens — cititoarele de ecran le detectează și accentuează cuvintele când citesc cu voce tare.
  • <b> / <i> sunt pur vizuale, fără sens semantic.

Folosește <strong> și <em> implicit. Folosește <b> sau <i> doar când vrei efectul vizual fără sensul semantic (rar — de exemplu, să italicizezi titlul unei cărți unde nu e implicată nicio accentuare reală).

Exemplu explicat
Formatare inline în acțiune
<p>Ca să instalezi pachetul, rulează <code>npm install react</code>
în terminal. <strong>Nu uita</strong> să incluzi flag-ul
<code>--save</code> în proiectele mai vechi. Asta instalează
<em>cea mai recentă versiune stabilă</em>, care la momentul
scrierii e <mark>18.2.0</mark>.</p>
Randat, asta devine un paragraf unde fragmentele de cod ies în evidență cu monospace, avertismentul e bold, „cea mai recentă versiune stabilă" e italic, iar numărul versiunii e evidențiat.

Line break-uri și linii orizontale

<br> forțează un line break. E self-closing. Folosește-l rar — de cele mai multe ori, paragrafele gestionează break-urile automat.

<p>Strada Principală 123<br>
București, 010101<br>
România</p>

Adrese, versuri de poezie, versuri de cântec — astea sunt utilizările legitime. Nu folosi <br> să creezi spațiu între paragrafe; folosește două tag-uri <p> separate.

<hr> (linie orizontală) creează o pauză tematică între secțiuni. Browserele îl randează ca o linie orizontală. Folosește-l când conținutul schimbă subiectul în aceeași pagină.

Citate: <blockquote> și <q>

<blockquote> e pentru pasaje citate — când reproduci ceva dintr-o altă sursă. Browserele îl indentează de obicei.

<blockquote cite="https://example.com/discurs">
  <p>Singura cale să faci muncă grozavă e să iubești ce faci.</p>
  <p>— Steve Jobs</p>
</blockquote>

<q> e pentru citate inline într-o propoziție. Browserele adaugă automat ghilimele în jurul lui.

O mică verificare semantică

De fiecare dată când întinzi mâna spre un tag, întreabă: tag-ul descrie ce e conținutul, sau doar cum arată? HTML-ului îi pasă de „ce". Dacă vrei doar ca ceva să arate într-un anumit fel, aia e treaba CSS-ului (vine în Modulul 2).

Ăsta e cel mai important obicei mental din HTML. Fă-l corect și paginile tale vor fi mai accesibile, se vor clasa mai bine în căutare și vor fi mai ușor de stilizat.

Alege tag-ul potrivit
Vrei să avertizezi utilizatorii despre o problemă critică de securitate cu text bold. Care tag e cel mai potrivit?
Recapitulare
  • Folosește un <h1> per pagină; folosește <h2> până la <h6> pentru ierarhie logică, niciodată sărind niveluri
  • <p> e pentru paragrafe; browser-ul gestionează încadrarea liniilor
  • Preferă <strong> față de <b> și <em> față de <i> — poartă sens
  • <code> pentru termeni tehnici și fragmente, <mark> pentru evidențieri
  • Folosește <blockquote> pentru citate bloc, <q> pentru citate inline, <br> rar
Modulul 1 · Lecția 39 min citire

Liste și tabele

Bullet points. Pași numerotați. Date structurate în rânduri și coloane. Aceste trei lucruri apar practic pe orice site. HTML are tag-uri dedicate pentru fiecare, iar alegerea celui potrivit contează mai mult decât realizează începătorii.

Liste neordonate: <ul> și <li>

Când ordinea elementelor nu contează, folosește o listă neordonată. Browserele o randează cu bullet points.

<ul>
  <li>Cafea</li>
  <li>Lapte</li>
  <li>Pâine</li>
</ul>

Două lucruri de internalizat:

  • <ul> e containerul. Împachetează toată lista.
  • <li> e fiecare element. Fiecare element de listă trăiește într-un <li>.

Liste ordonate: <ol> și <li>

Când ordinea contează (pași dintr-o rețetă, un clasament, instrucțiuni), folosește o listă ordonată. Browserele o randează cu numere.

<ol>
  <li>Preîncălzește cuptorul la 200°C</li>
  <li>Amestecă făina cu apa</li>
  <li>Coace 20 de minute</li>
</ol>
Alegerea între ul și ol

Regula nu e „bullet points vs numere". Regula e: schimbând ordinea s-ar schimba sensul?

  • Listă de cumpărături? Ordinea nu contează → <ul>
  • Pași de rețetă? Ordinea e crucială → <ol>
  • Top 10 filme? Ordinea e tot rostul → <ol>
  • Feature-uri de produs? Ordinea e de obicei arbitrară → <ul>

Poți oricând să stilizezi un <ul> să arate numere cu CSS, sau un <ol> să nu arate deloc markere. Alegerea e despre sens, nu aspect.

Liste imbricate

Listele pot intra în alte liste. Păstrează imbricarea logică și lizibilă.

<ul>
  <li>Frontend
    <ul>
      <li>HTML</li>
      <li>CSS</li>
      <li>JavaScript</li>
    </ul>
  </li>
  <li>Backend
    <ul>
      <li>Node.js</li>
      <li>PHP</li>
    </ul>
  </li>
</ul>

Lista imbricată trebuie să intre într-un <li>, nu între elemente <li>. E o greșeală comună.

Tabele: <table>, <tr>, <th>, <td>

Tabelele sunt pentru date tabulare — informații care au rânduri și coloane reale, ca un spreadsheet. Gândește-te la cifre de vânzări, programe, specificații de produs.

Cele patru tag-uri de care ai nevoie:

  • <table> — containerul
  • <tr> — un rând de tabel
  • <th> — o celulă de antet (de obicei primul rând și/sau prima coloană)
  • <td> — o celulă de date
<table>
  <tr>
    <th>Luna</th>
    <th>Vânzări</th>
    <th>Creștere</th>
  </tr>
  <tr>
    <td>Ianuarie</td>
    <td>€12,400</td>
    <td>+5%</td>
  </tr>
  <tr>
    <td>Februarie</td>
    <td>€14,200</td>
    <td>+15%</td>
  </tr>
</table>

Structură proprie: <thead>, <tbody>, <tfoot>

Pentru tabele reale, împachetează rândurile de antet în <thead> și rândurile de body în <tbody>. Face structura mai clară atât pentru oameni cât și pentru tehnologii asistive.

Niciodată nu folosi tabele pentru layout

Există o eră infamă a web design-ului (aproximativ 1997-2007) în care oamenii foloseau <table> pentru a poziționa totul pe o pagină — navigare, sidebar-uri, layout-uri întregi.

Nu face asta. Folosește tabele doar pentru date tabulare reale — date care au natural rânduri și coloane și unde relația între celule contează.

Pentru layout (aranjarea vizuală pe pagină), vom folosi Flexbox și Grid în Modulele 4 și 5. Astea sunt uneltele potrivite pentru job.

Alege corect
Faci o listă cu top 5 cele mai mari filme din 2025. Ce tip de listă se potrivește cel mai bine?
Recapitulare
  • <ul> pentru elemente neordonate, <ol> pentru ordonate — bazat pe dacă ordinea poartă sens
  • Imbrică liste punând lista-copil în <li>, nu între elemente
  • Tabelele sunt doar pentru date tabulare, niciodată pentru layout de pagină
  • Folosește <thead> și <tbody> pentru claritate
Modulul 1 · Lecția 49 min citire

Link-uri și navigare

„H"-ul din HTML vine de la Hypertext — text cu link-uri. Link-urile sunt ce au făcut web-ul un sistem interconectat, nu o grămadă de documente deconectate. Ai dat click pe milioane dintre ele. Acum le vei scrie.

Ancora de bază: <a>

Link-urile folosesc tag-ul <a> (ancoră). Destinația merge în atributul href.

<a href="https://example.com">Vezi Example</a>

Orice e între tag-ul de deschidere și cel de închidere devine clickabil — text, imagini, chiar și secțiuni întregi de pagină.

Link-uri interne vs externe

Există două moduri comune de a scrie href-ul:

Link-uri externe merg la alt site. Folosește URL-ul complet cu protocol (https://).

<a href="https://google.com">Google</a>
<a href="https://github.com/vercel/next.js">Next.js pe GitHub</a>

Link-uri interne merg la altă pagină de pe același site. Folosește o cale relativă la pagina curentă.

<a href="/despre.html">Despre noi</a>
<a href="/produse/tastatura.html">Tastaturile noastre</a>

Slash-ul / la început înseamnă „de la rădăcina acestui site". Fără el, browser-ul caută relativ la folder-ul paginii curente.

Capcana cu calea

Trei greșeli comune cu căile:

  • Lipsa protocolului la link-uri externe. href="example.com" NU merge la example.com — browser-ul îl interpretează ca link intern relativ. Include mereu https://.
  • Uitarea slash-ului inițial. href="despre.html" vs href="/despre.html" — al doilea e neambiguu (întotdeauna de la rădăcină). Primul depinde unde ești în site.

Link-uri de ancoră: sari în interiorul paginii

Poți face link la o secțiune specifică folosind hash (#) urmat de un id pe elementul țintă.

<h2 id="preturi">Prețurile noastre</h2>

<a href="#preturi">Vezi prețurile</a>

Când dai click, browser-ul derulează la elementul cu acel ID. Asta face să funcționeze link-urile „Înapoi sus", navigarea tip cuprins și site-urile lungi de o singură pagină.

Atributul target

Implicit, clicul pe un link înlocuiește pagina curentă. Ca să deschizi link-ul într-un tab nou, folosește target="_blank".

<a href="https://example.com" target="_blank">Deschide în tab nou</a>

Când să folosești target="_blank":

  • Link-uri la site-uri externe (utilizatorii vor adesea să țină site-ul tău deschis)
  • Link-uri la PDF-uri sau fișiere descărcabile
  • Link-uri deschise într-un flux de completare formular pe care nu vrei să-l întrerupi

Atributul rel cu target="_blank"

Când folosești target="_blank", adaugă și rel="noopener noreferrer".

<a href="https://example.com"
   target="_blank"
   rel="noopener noreferrer">Link extern</a>

Asta protejează utilizatorii de două probleme:

  • noopener împiedică tab-ul nou să poată deturna sau controla tab-ul original (problemă reală de securitate).
  • noreferrer împiedică site-ul țintă să vadă de pe ce pagină a venit utilizatorul.

E boilerplate. Scrie-l de fiecare dată.

Link-uri la email-uri și numere de telefon

Două tipuri speciale de link-uri pentru info de contact:

Email-uri folosesc mailto:.

<a href="mailto:salut@example.com">Trimite-ne email</a>

Clicul deschide client-ul de email implicit al utilizatorului cu adresa pre-completată.

Telefoane folosesc tel:.

<a href="tel:+40712345678">+40 712 345 678</a>

Pe un telefon, clicul oferă să sune la număr. Pe desktop, poate deschide Skype, WhatsApp sau nimic — e ok.

Scrierea unor texte bune de link

Cuvintele alese pentru textul link-ului contează — pentru uzabilitate, accesibilitate și SEO.

Text bun de link descrie ce va găsi utilizatorul la celălalt capăt:

<a href="/preturi">Vezi prețurile noastre</a>
<a href="/ghid/flexbox">Citește ghidul Flexbox</a>

Text prost de link e vag:

<a href="/preturi">Click aici</a>
<a href="/ghid/flexbox">Citește mai mult</a>

Cititoarele de ecran anunță adesea link-urile separat de textul din jur. Un utilizator nevăzător navigând doar prin link-uri aude „click aici, click aici, citește mai mult" fără context. Text descriptiv rezolvă asta.

Repară link-ul
Care din astea e modul corect de a face link la un site extern într-un tab nou?
Recapitulare
  • <a href="..."> creează un link; conținutul dintre tag-uri devine clickabil
  • Folosește URL-uri complete cu https:// pentru link-uri externe, căi pentru interne
  • Link-urile de ancoră (#id-sectiune) derulează la elemente cu id corespunzător
  • Adaugă target="_blank" pentru taburi noi, mereu cu rel="noopener noreferrer"
  • Folosește mailto: pentru email-uri, tel: pentru telefoane
  • Textul link-ului ar trebui să descrie destinația, nu să spună „click aici"
Modulul 1 · Lecția 510 min citire

Imagini și media

O pagină web fără imagini se simte neterminată. Din fericire, adăugarea imaginilor e unul dintre cele mai simple lucruri din HTML — și unul dintre cele mai ușoare locuri unde greșești. O pagină de 100MB pentru că nimeni n-a comprimat imaginea hero e un rit de trecere pe care nu-l doriți.

Hai să facem imaginile bine de la început.

Tag-ul <img>

Adăugarea unei imagini ia un singur tag self-closing:

<img src="poza.jpg" alt="Un apus peste ocean">

Două atribute contează de fiecare dată:

src — sursa imaginii. Poate fi o cale relativă la un fișier din proiectul tău, sau un URL complet la o imagine externă.

alt — text alternativ. O descriere scurtă a imaginii pentru cititoarele de ecran și pentru când imaginea nu se încarcă. Scrie mereu text alt cu sens; să-l sari e rău pentru accesibilitate.

Scrierea unui text alt bun

Textul alt ar trebui să descrie ce transmite imaginea, nu că e o imagine.

<!-- Rău: -->
<img src="ceo.jpg" alt="Imagine cu CEO-ul nostru">

<!-- Bun: -->
<img src="ceo.jpg" alt="Maria Popescu, CEO, zâmbind la biroul ei">

<!-- Și bun (imagine pur decorativă): -->
<img src="separator.svg" alt="">

Când o imagine e pur decorativă (un separator liniar, o iconiță lângă un text), folosește alt="" — un șir gol. Asta spune cititoarelor de ecran să o sară. Nu omite atributul complet — asta face cititoarele de ecran să încerce să citească numele fișierului.

Formate de imagine: care când

Un ghid rapid:

  • JPEG (.jpg) — fotografii. Se comprimă bine, arată grozav pentru tonuri continue. Nu folosi pentru logouri sau text.
  • PNG (.png) — imagini cu transparență, logo-uri, capturi de ecran. Fișiere mai mari decât JPEG pentru poze, dar margini clare.
  • WebP (.webp) — format modern, mai mic decât JPEG/PNG la calitate egală. Suportat în orice browser modern. Folosește-l implicit pentru fotografii.
  • AVIF (.avif) — chiar mai mic decât WebP. Mai nou, dar suportat destul de larg.
  • SVG (.svg) — grafică vectorială (logouri, iconițe, diagrame). Scalează la orice dimensiune fără pierdere. Fișiere minuscule pentru forme geometrice.

Regula pentru 2026: folosește WebP pentru fotografii, SVG pentru logouri și iconițe, PNG doar dacă ai specific nevoie de raster cu transparență.

Atributele width și height

Setează mereu width și height pe imaginile tale — în pixeli bruti, potrivind dimensiunile naturale ale fișierului.

<img src="hero.webp" alt="Atelierul nostru" width="1200" height="800">

De ce? Pentru că browser-ul folosește aceste dimensiuni să rezerve spațiu pentru imagine înainte să se încarce. Fără ele, conținutul paginii sare în timp ce imaginile sosesc, creând o experiență deranjantă numită layout shift.

Lazy loading

Pentru imaginile de sub fold (nu vizibile la încărcarea paginii), folosește loading="lazy". Browser-ul așteaptă să le descarce până ce utilizatorul derulează aproape.

<img src="galerie-1.webp" alt="Foto galerie 1"
     width="800" height="600" loading="lazy">

Nu folosi loading="lazy" pe imaginile de deasupra fold-ului (imaginea hero, logo-ul) — acelea ar trebui să se încarce imediat pentru cea mai bună primă impresie.

Imagini responsive cu <picture>

Ecrane diferite merită imagini diferite — ecranele mici n-au nevoie de imaginea hero 4K făcută pentru desktop.

Elementul <picture> îți lasă să oferi surse multiple. Browser-ul alege cea mai bună în funcție de dimensiunea viewport-ului, densitatea pixelilor, sau formatele suportate.

<picture>
  <source srcset="hero-mobile.webp" media="(max-width: 600px)">
  <source srcset="hero-desktop.webp" media="(min-width: 601px)">
  <img src="hero-desktop.jpg" alt="Atelier la apus"
       width="1200" height="800">
</picture>

<img>-ul din <picture> e fallback-ul — include-l mereu. Browserele care nu înțeleg <picture> sau nu se potrivesc cu nicio <source> cad înapoi la <img>-ul simplu.

Exemplu explicat
Embed modern de imagine
<picture>
  <source type="image/avif" srcset="hero.avif">
  <source type="image/webp" srcset="hero.webp">
  <img src="hero.jpg"
       alt="Un fotograf lucrând în studio"
       width="1600"
       height="900"
       loading="lazy">
</picture>
Browser-ul încearcă AVIF primul, cade înapoi la WebP, cade înapoi la JPEG. Fiecare primește cel mai mic fișier pe care browser-ul lui îl suportă. Imaginea e lazy-loaded, cu dimensiuni corecte, cu text alt descriptiv. Ia mai multe linii decât <img src="hero.jpg">, dar e abordarea profesională.

Video: <video>

Tag-ul video HTML gestionează redarea video nativ:

<video src="intro.mp4" controls width="800" height="450">
  Browser-ul tău nu suportă redarea video.
</video>
  • controls arată controalele implicite (play/pause, volum)
  • autoplay pornește automat. Folosește cu grijă.
  • muted pornește fără sunet.
  • loop reia la final.
  • poster="img.jpg" arată o imagine specifică înainte să pornească.

Audio: <audio>

Tipar similar pentru audio:

<audio src="podcast.mp3" controls>
  Browser-ul tău nu suportă redarea audio.
</audio>

Figuri cu subtitluri: <figure> și <figcaption>

Când o imagine are un subtitlu, împachetează-le împreună în <figure>:

<figure>
  <img src="grafic-2024.png" alt="Grafic creștere venituri 40% YoY"
       width="800" height="500">
  <figcaption>Fig. 1: Creșterea veniturilor în 2024</figcaption>
</figure>
Găsește problema
Care dintre aceste tag-uri imagine are o problemă ce ar putea cauza o experiență proastă?
Recapitulare
  • Folosește <img src="..." alt="..."> — include mereu alt (șir gol pentru decorative)
  • WebP pentru fotografii, SVG pentru logo-uri și iconițe
  • Setează mereu width și height pentru a preveni layout shift
  • Folosește loading="lazy" pentru imagini sub fold, niciodată pe hero
  • Folosește <picture> pentru imagini responsive și fallback-uri de format
Modulul 1 · Lecția 611 min citire

HTML semantic

Ai putea construi un site întreg folosind doar tag-uri <div>. Ar arăta identic cu unul construit cu HTML semantic corect. Dar o pagină semantică e dramatic mai bună pentru motoarele de căutare, cititoarele de ecran și orice om care-ți citește codul mai târziu.

Lecția asta e unde developerii buni se separă de cei okay. Fă asta corect și tot ce urmează devine mai ușor.

Ce înseamnă „semantic"

Un tag e semantic când numele lui descrie sensul conținutului, nu aspectul. Compară:

<!-- Non-semantic: -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>
<div class="footer">...</div>

<!-- Semantic: -->
<header>...</header>
<nav>...</nav>
<main>...</main>
<footer>...</footer>

Browser-ul le randează identic. Dar a doua versiune spune ce e fiecare secțiune — nu doar „o cutie". Acea informație e de neprețuit pentru cititoare de ecran, motoare de căutare și pentru oricine menține codul tău.

Elementele semantice mari

<header>

Secțiunea introductivă de sus a unei pagini, sau de sus a unei secțiuni. Conține de obicei logo-ul site-ului, navigarea principală, poate un slogan.

<header>
  <img src="logo.svg" alt="Compania noastră">
  <nav>
    <a href="/">Acasă</a>
    <a href="/despre">Despre</a>
    <a href="/contact">Contact</a>
  </nav>
</header>

<nav>

O secțiune conținând link-uri de navigare. Folosește-l pentru navigarea principală a site-ului, sidebar, cuprins, paginare — oriunde ai un grup de link-uri care ghidează utilizatorii.

<main>

Conținutul principal al paginii. Un singur <main> per pagină — marchează conținutul unic, față de lucruri partajate ca header și footer.

Cititoarele de ecran au un shortcut să sară direct la <main>, sărind peste navigare. E un ajutor mare pentru utilizatorii care navighează din tastatură.

<article>

O bucată de conținut self-contained care ar putea sta singură — un articol de blog, o știre, un listing de produs, un comentariu.

<article>
  <h2>Cum centrezi un div în 2026</h2>
  <p>Ani de zile, centrarea lucrurilor în CSS a fost un meme...</p>
</article>

Test: dacă ai putea sindica această bucată pe alt site (un feed RSS, un newsletter), probabil merită <article>.

<section>

O secțiune generică de conținut — o grupare tematică. De obicei are propriul titlu.

Diferența față de <article>: <section> e o parte dintr-un întreg mai mare; <article> e standalone.

<aside>

Conținut tangențial legat de conținutul principal. Sidebar-uri, link-uri conexe, cutii de callout, biografii de autor lângă articole.

<footer>

Secțiunea finală de jos a unei pagini sau secțiuni. Ține de obicei copyright, info contact, navigare secundară, link-uri sociale.

Exemplu explicat
O structură de pagină semantică completă
<body>
  <header>
    <img src="logo.svg" alt="Logo companie">
    <nav aria-label="Navigare principală">
      <a href="/">Acasă</a>
      <a href="/blog">Blog</a>
    </nav>
  </header>

  <main>
    <article>
      <header>
        <h1>Starea CSS-ului în 2026</h1>
        <p>Postat pe 12 aprilie 2026</p>
      </header>

      <section>
        <h2>Container queries</h2>
        <p>Livrate în fine în toate browserele...</p>
      </section>

      <footer>
        <p>Scris de Alex Ionescu</p>
      </footer>
    </article>

    <aside>
      <h3>Postări recente</h3>
    </aside>
  </main>

  <footer>
    <p>© 2026 Compania mea</p>
  </footer>
</body>
Observă că <article>-ul are propriul <header> și <footer> — sunt limitate la articol, nu la pagina întreagă. E ok și încurajat.

Când să folosești totuși <div> și <span>

Tag-urile semantice acoperă majoritatea nevoilor structurale. Dar <div> și <span> nu-s deprecate — sunt încă corecte pentru grupare pur vizuală care nu poartă sens semantic.

  • <div> — un container block-level generic. Folosește când ai nevoie să grupezi elemente pentru scopuri de stilizare CSS și niciun tag semantic nu se potrivește.
  • <span> — un container inline generic. Folosește când ai nevoie să stilizezi o bucată de text într-o linie.
div-itis

Un anti-pattern comun: împachetarea totului în <div> cu nume de clase precum class="header", class="main-content", class="sidebar". Numele claselor descriu sensul, dar tag-ul nu.

Dacă te prinzi dându-i unui <div> o clasă care se potrivește cu numele unui tag semantic (header, nav, footer), e un semnal puternic că ar trebui să folosești tag-ul semantic.

De ce contează semantica

Trei motive concrete:

1. SEO. Motoarele de căutare folosesc tag-urile semantice să înțeleagă despre ce e pagina ta. O pagină cu <article>, <header> și <main> corecte dă Google-ului o hartă mai clară a conținutului.

2. Accesibilitate. Utilizatorii de cititoare de ecran navighează prin landmark-uri — „sari la main", „listează toate titlurile", „listează toate navigările". Tag-urile semantice oferă acele landmark-uri automat.

3. Mentenabilitate. Peste șase luni, când tu (sau altcineva) deschizi codul ăsta, <article> e auto-explicativ. <div class="post-wrapper-inner"> nu.

Alege tag-ul potrivit
Construiești o pagină cu listing de produse. Fiecare produs are titlu, imagine, descriere și preț. Care tag reprezintă cel mai bine un produs individual?
Recapitulare
  • Tag-urile semantice descriu sensul, nu aspectul
  • Folosește <header>, <nav>, <main> (o dată per pagină), <article>, <section>, <aside>, <footer>
  • <div> și <span> sunt ok pentru grupare pur vizuală
  • Înlocuiește <div class="header"> cu <header>
Modulul 1 · Lecția 711 min citire

Formulare

Formularele sunt cum utilizatorii îți răspund la site — înregistrare, login, trimitere mesaje, plasare comenzi. Sunt și unul dintre cele mai comune locuri unde paginile îi eșuează pe utilizatori: câmpuri confuze, label-uri lipsă, validare stricată.

HTML-ul modern are toate uneltele să construiești formulare care funcționează bine și se simt native.

Tag-ul <form>

Fiecare formular e împachetat într-un element <form>:

<form action="/submit" method="POST">
  <!-- câmpurile merg aici -->
</form>
  • action — URL-ul care primește datele trimise (gestionat de un server).
  • method — de obicei POST (pentru trimitere date) sau GET (pentru trimitere interogări ca parametri URL, ca formularele de căutare).

Label-uri și inputuri

Fiecare input are nevoie de un label. Mereu. Fără excepție.

<label for="email">Adresă email</label>
<input type="email" id="email" name="email">

Atributul for de pe <label> se potrivește cu id-ul de pe <input>. Asta le leagă, ceea ce înseamnă:

  • Click pe label focusează input-ul
  • Cititoarele de ecran anunță label-ul când utilizatorul focusează input-ul
  • Label-ul devine o țintă mai mare de click, îmbunătățind UX-ul mobil
Placeholder nu e un label

Nu folosi placeholder ca înlocuitor pentru un label.

Placeholder-urile dispar când utilizatorul începe să tasteze, ceea ce face greu să le referezi în timp ce completezi formularul. Au și contrast slab implicit (rău pentru accesibilitate). Folosește placeholder pentru un indiciu sau exemplu, nu pentru numele câmpului.

Numeroasele tipuri de input

Atributul type spune browser-ului ce tip de date vrei, care activează tastaturi specifice pe mobil, validare și widget-uri.

<input type="text">          <!-- Text simplu -->
<input type="email">         <!-- Email, validează format -->
<input type="password">      <!-- Parolă, caractere ascunse -->
<input type="tel">           <!-- Telefon, arată tastatură numerică -->
<input type="url">           <!-- URL, validează format -->
<input type="number">        <!-- Doar numere, cu săgeți sus/jos -->
<input type="date">          <!-- Selector de dată -->
<input type="color">         <!-- Selector de culoare -->
<input type="file">          <!-- Buton upload fișier -->
<input type="checkbox">      <!-- Toggle da/nu -->
<input type="radio">         <!-- Alegere exclusivă între opțiuni -->
<input type="range">         <!-- Slider -->

Folosește tipul potrivit de fiecare dată. type="email" pe mobil arată o tastatură cu tasta @; type="tel" arată un pad numeric. Ăsta e un câștig gratuit de usability.

Câmpuri obligatorii și validare

Adaugă required ca să faci un câmp obligatoriu:

<input type="email" id="email" name="email" required>

Browser-ul blochează trimiterea până ce câmpurile obligatorii sunt completate.

Alte atribute de validare încorporate:

<!-- Lungime minimă și maximă pentru text: -->
<input type="text" minlength="3" maxlength="50">

<!-- Valori minime și maxime pentru numere: -->
<input type="number" min="18" max="120">

Checkbox-uri și radio-buttons

Checkbox-urile sunt pentru alegeri da/nu sau multiple:

<fieldset>
  <legend>Interese</legend>
  <label>
    <input type="checkbox" name="interese" value="web">
    Web development
  </label>
  <label>
    <input type="checkbox" name="interese" value="mobile">
    Mobile development
  </label>
</fieldset>

Radio-buttons sunt pentru exact-una-dintre-câteva alegeri. Atributul name partajat le grupează:

<fieldset>
  <legend>Plan</legend>
  <label>
    <input type="radio" name="plan" value="free">
    Gratuit
  </label>
  <label>
    <input type="radio" name="plan" value="pro">
    Pro (10€/lună)
  </label>
</fieldset>

<fieldset> grupează inputuri legate. <legend> descrie pentru ce e grupul. Ambele îmbunătățesc accesibilitatea.

Dropdown-uri: <select> și <option>

Pentru selectarea unui element dintr-o listă:

<label for="tara">Țară</label>
<select id="tara" name="tara">
  <option value="">Selectează o țară</option>
  <option value="ro">România</option>
  <option value="de">Germania</option>
  <option value="fr">Franța</option>
</select>

Text pe mai multe linii: <textarea>

Pentru input de text mai lung ca mesaje sau descrieri:

<label for="mesaj">Mesajul tău</label>
<textarea id="mesaj" name="mesaj" rows="5" cols="40"
          placeholder="Spune-ne ce crezi..."></textarea>

Butonul de submit

Fiecare formular are nevoie de o cale să trimită:

<button type="submit">Trimite mesajul</button>
Exemplu explicat
Un formular complet din viața reală
<form action="/contact" method="POST">
  <h2>Contactează-ne</h2>

  <label for="nume">Nume</label>
  <input type="text" id="nume" name="nume" required>

  <label for="email">Email</label>
  <input type="email" id="email" name="email" required>

  <label for="mesaj">Mesaj</label>
  <textarea id="mesaj" name="mesaj" rows="5" required
            minlength="10" maxlength="500"></textarea>

  <label>
    <input type="checkbox" name="abonare">
    Trimiteți-mi actualizări ocazionale
  </label>

  <button type="submit">Trimite mesajul</button>
</form>
Fiecare input are un label. Câmpurile obligatorii sunt marcate. Câmpul de email validează formatul email. Mesajul are lungime min și max. Totul funcționează cu tastatură și cititoare de ecran. Așa arată un formular corect.
Ce tip e potrivit?
Construiești un formular unde utilizatorii introduc vârsta (doar 18+). Care setup de input e corect?
Recapitulare
  • Fiecare formular e împachetat în <form action="..." method="...">
  • Fiecare input are nevoie de un <label>, asociat prin for/id sau prin împachetare
  • Alege tipul potrivit — activează tastaturi mobile și validare browser
  • Adaugă required, minlength/maxlength, min/max, pattern pentru validare nativă
  • Grupează inputuri legate cu <fieldset> și <legend>
  • Trimite cu <button type="submit">
Modulul 1 · Lecția 810 min citire

Atribute globale & proiect

Majoritatea atributelor HTML funcționează doar pe tag-uri specifice — href pe link-uri, src pe imagini, type pe inputuri. Dar o mână de atribute funcționează pe orice tag, iar astea sunt cele pe care le vei folosi zilnic, în special pe măsură ce înveți CSS și JavaScript.

Hai să le învățăm, apoi să punem totul din modulul ăsta împreună într-un proiect real.

Atribute globale pe care le vei folosi zilnic

id — un identificator unic

id dă unui element un nume unic. Fiecare id trebuie să fie unic per pagină — două elemente nu pot împărți același id.

<h2 id="preturi">Prețurile noastre</h2>
<section id="faq">
  ...
</section>

Pentru ce e id:

  • Link-uri de ancoră<a href="#preturi">Vezi prețurile</a> sare la elementul cu id="preturi"
  • Label-uri cu for — conectarea label-urilor la inputuri
  • Țintire JavaScriptdocument.getElementById('faq')

Reguli pentru valori bune de id:

  • Folosește litere mici cu cratime: sectiune-hero, nu SectiuneHero sau sectiune_hero
  • Fă-le descriptive: nav-principala, nu nav1
  • Păstrează-le unice pe pagină

class — un marcaj reutilizabil

class îți permite să etichetezi elemente ca să aplici aceeași stilizare grupurilor.

<article class="card">...</article>
<article class="card featured">...</article>
<article class="card">...</article>

Al treilea element are două clase: card și featured. Mai multe clase sunt separate prin spații.

Ăsta e atributul pe care-l vei folosi cel mai mult în tot web development-ul.

id vs class, în practică

Regula din manual e „id e unic, class e reutilizabil". Adevărat tehnic, dar regula practică e:

Folosește clase pentru stilizare. Folosește id pentru identificare.

Chiar și un element unic pe o pagină (logo-ul principal) ar trebui stilizat printr-o clasă — nu știi niciodată când vei vrea același stil în altă parte. Rezervă id pentru când ai nevoie să legi sau să găsești programatic acel element specific.

data-* — date personalizate

Ai nevoie să stochezi informații pe un element pentru ca JavaScript să le citească mai târziu? Folosește un atribut data-*. Orice e după data- poate fi ce vrei.

<button data-user-id="42" data-action="delete">Șterge user</button>

<article data-category="tehnologie" data-published="2026-04-21">
  <h2>Framework nou lansat</h2>
</article>

Proiectul Despre Mine

Știi acum destul HTML să construiești ceva real. Ăsta e primul tău proiect: o pagină personală „Despre Mine" folosind doar HTML — fără CSS încă.

Pagina ar trebui să includă:

  1. Un antet principal cu numele tău și un slogan
  2. O navigare cu cel puțin 3 link-uri de ancoră (Despre, Skills, Contact)
  3. O secțiune main cu cel puțin trei articole sau secțiuni:
    • Despre mine: câteva paragrafe cu formatare inline
    • Skills: o listă neordonată cu skills-urile tale
    • Contact: un formular cu câmpuri nume, email și mesaj
  4. Un footer cu copyright și link-uri sociale

Cerințe:

  • Folosește tag-uri semantice corecte (<header>, <nav>, <main>, <section>, <footer>)
  • Fiecare imagine are un atribut alt
  • Fiecare input de formular are un <label>
  • Folosește cel puțin un <strong>, un <em> și un <code> undeva
  • Folosește link-uri de ancoră care chiar derulează la secțiuni cu ID-uri corespunzătoare
Exemplu explicat
Un starter pentru pagina ta Despre Mine
<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Alex Ionescu — Frontend developer</title>
</head>
<body>
  <header>
    <h1>Alex Ionescu</h1>
    <p>Frontend developer care învață în fiecare zi</p>
    <nav aria-label="Secțiuni pagină">
      <ul>
        <li><a href="#despre">Despre</a></li>
        <li><a href="#skills">Skills</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <section id="despre">
      <h2>Despre mine</h2>
      <p>Învăț web development prin cursul <strong>Școala
      de WordPress</strong>. Am lucrat în <em>marketing</em>,
      dar mereu am fost curios cum sunt construite site-urile.</p>
    </section>

    <section id="skills">
      <h2>Skills pe care le construiesc</h2>
      <ul>
        <li><code>HTML</code> — structură și semantică</li>
        <li><code>CSS</code> — layout, tipografie, responsive</li>
        <li><code>JavaScript</code> — urmează</li>
      </ul>
    </section>

    <section id="contact">
      <h2>Contactează-mă</h2>
      <form action="/contact" method="POST">
        <label for="nume">Nume</label>
        <input type="text" id="nume" name="nume" required>

        <label for="email">Email</label>
        <input type="email" id="email" name="email" required>

        <label for="mesaj">Mesaj</label>
        <textarea id="mesaj" name="mesaj" rows="5" required></textarea>

        <button type="submit">Trimite</button>
      </form>
    </section>
  </main>

  <footer>
    <p>© 2026 Alex Ionescu</p>
    <p>Construit în timp ce învăț HTML.</p>
  </footer>
</body>
</html>
Deschide asta în VS Code. Rulează-l cu Live Server. Personalizează totul.

Cum va arăta pagina ta

Fără niciun CSS, pagina ta va arăta ca ceva din 1995 — text negru Times New Roman, link-uri albastre subliniate, spațiere implicită peste tot. Asta e exact corect la acest nivel.

Scopul nu e s-o faci frumoasă încă. Scopul e s-o faci corectă: semantică, accesibilă, structurată. În Modulul 2 începem să adăugăm CSS și vedem acea pagină urâtă transformată în ceva frumos.

Rezistă impulsului să adaugi CSS

Știi puțin CSS. Ai putea strecura un bloc <style> și s-o faci frumoasă. Nu o face.

Obiceiul de a construi întâi structura, apoi să stilizezi e unul dintre cei mai mari separatori între începători și profesioniști. Începătorii hack-uiesc HTML și CSS simultan și ajung cu cod greu de menținut. Profesioniștii fac HTML-ul corect întâi, apoi adaugă stilurile deasupra.

Recapitulare
  • id e unic per pagină — folosește pentru link-uri de ancoră, ținte label, și JavaScript
  • class e reutilizabil — folosește pentru stilizare (îl vei folosi cel mai mult)
  • title adaugă tooltip-uri (limitate pe mobil)
  • data-* stochează date personalizate citibile de JavaScript
  • Proiect Modulul 1: construiește pagina Despre Mine folosind tot ce ai învățat — fără CSS încă

🎉 Ai terminat Modulul 1!
Paginile tale au structură reală acum. Modulul 2 (CSS) adaugă stil și transformă totul în ceva frumos.

Modulul 2 · Lecția 18 min citire

Cum adaugi CSS la HTML

Ai construit pagini cu HTML semantic. Sunt structurate, accesibile și mai urâte decât un site geocities din 2002. Urmează să schimbăm asta.

CSS — Cascading Style Sheets — e cum transformăm acele pagini brute în ceva care arată proiectat. În acest modul înveți fundamentele. Hai să începem cu cea mai simplă întrebare posibilă: unde pui CSS-ul?

Trei moduri de a adăuga CSS

Poți adăuga stiluri la o pagină HTML în trei moduri. Doar unul e calea corectă pentru proiecte reale, dar ar trebui să le recunoști pe toate trei.

1. Stiluri inline (cel mai rău pentru muncă reală)

Pui stilul direct pe element folosind atributul style:

<h1 style="color: red; font-size: 32px;">Salut</h1>

Pro: rapid de scris, evident ce se stilizează.

Contra: imposibil de reutilizat, imposibil de întreținut, aglomerează HTML-ul, imposibil să stilizezi stări pseudo ca :hover.

Când să folosești: aproape niciodată.

2. Stylesheet intern (uneori util)

Pui CSS într-un bloc <style> în <head>:

<head>
  <title>Pagina mea</title>
  <style>
    h1 {
      color: red;
      font-size: 32px;
    }
  </style>
</head>

Pro: totul într-un singur fișier, bun pentru experimente rapide.

Contra: stilurile nu pot fi partajate între pagini. Dacă ai 10 pagini care ar trebui să arate la fel, ar trebui să lipești același bloc <style> în toate 10.

3. Stylesheet extern (calea corectă)

Pui CSS într-un fișier .css separat și îl linkuiești din HTML:

index.html
<head>
  <title>Pagina mea</title>
  <link rel="stylesheet" href="styles.css">
</head>
styles.css
h1 {
  color: red;
  font-size: 32px;
}

Pro: un singur stylesheet stilizează fiecare pagină din site-ul tău; browser-ul cache-uiește fișierul deci se încarcă o dată; separarea preocupărilor (HTML e structură, CSS e stil); mai ușor de întreținut.

Folosește stylesheet-uri externe 99% din timp. Intern pentru prototipuri rapide. Inline aproape niciodată.

Tag-ul link nu are închidere

Observă <link rel="stylesheet" href="styles.css"> — nu există </link>. Tag-ul <link> e self-closing, ca <img> și <meta>.

De asemenea, atributul rel="stylesheet" e obligatoriu — spune browser-ului ce tip de fișier e. Uită-l și stilurile tale nu se încarcă.

Anatomia unei reguli CSS

O regulă CSS are trei părți:

h1 {
  color: red;
  font-size: 32px;
}
  • Selectorh1 — spune browser-ului ce elemente să stilizeze
  • Proprietatecolor, font-size — ce aspect să schimbe
  • Valoarered, 32px — ce să-l schimbe la

Proprietățile și valorile sunt scrise ca proprietate: valoare;, în acolade. Fiecare declarație se termină cu punct-virgulă.

Comentarii în CSS

Scrie comentarii cu /* */:

/* Asta stilizează titlul principal */
h1 {
  color: #c8553d;
}

/* Text paragraf — dimensiune confortabilă pentru citit */
p {
  font-size: 18px;
  line-height: 1.6;
}

Regula de aur: separă preocupările

Marele principiu din spatele stylesheet-urilor externe e separarea preocupărilor:

  • HTML gestionează structura și conținutul
  • CSS gestionează aspectul
  • JavaScript gestionează comportamentul (mai târziu)

Când astea sunt amestecate, codul tău devine imposibil de întreținut. Când sunt separate, poți schimba aspectul întregului site editând un singur fișier CSS, fără să atingi HTML-ul.

Verificare rapidă
Construiești un site de 10 pagini unde fiecare pagină ar trebui să folosească aceleași stiluri. Care e abordarea potrivită?
Recapitulare
  • Trei moduri de a adăuga CSS: inline (style="..."), intern (bloc <style>), extern (<link> la fișier .css)
  • Stylesheet-urile externe sunt implicitul corect pentru proiecte reale
  • O regulă CSS = selector + { proprietate: valoare; }
  • Folosește /* */ pentru comentarii
  • Păstrează HTML și CSS în fișiere separate pentru mentenabilitate
Modulul 2 · Lecția 210 min citire

Selectori fundamentali

Selectorii CSS sunt cum spui browser-ului „stilizează chestia asta, nu aia". Îi faci corect și stilizarea unei pagini complexe devine o plăcere. Îi faci greșit și vei ajunge să scrii !important peste tot, întrebându-te de ce nu se aplică stilurile tale.

Selector de element — potrivește după numele tag-ului

Cel mai simplu selector. Țintește fiecare element cu un tag dat.

h1 {
  color: #c8553d;
}

p {
  line-height: 1.6;
}

a {
  color: #1e6091;
}

Fiecare <h1> de pe pagină devine roșu. Fiecare <p> primește line-height confortabil. Fiecare <a> devine albastru.

Când să folosești: pentru implicituri la nivelul întregii pagini. „Toate paragrafele ar trebui să aibă acest line-height."

Selector de clasă — potrivește după atributul class

Țintește elemente cu un atribut class specific. Scris cu un punct înainte.

<p class="lead">Paragraful principal.</p>
<p>Un paragraf obișnuit.</p>
<p class="lead">Alt paragraf principal.</p>
.lead {
  font-size: 20px;
  font-weight: 500;
  color: #333;
}

Doar paragrafele cu class="lead" primesc stilul lead. <p>-ul simplu din mijloc e neatins.

Ăsta e selectorul pe care-l vei folosi cel mai mult. Clasele îți permit să aplici aceeași stilizare oriunde ai nevoie, indiferent de tag.

Selector de ID — potrivește după atributul id

Țintește singurul element cu un id specific. Scris cu diez la început.

<main id="continut">
  <p>Conținutul principal aici.</p>
</main>
#continut {
  max-width: 800px;
  margin: 0 auto;
}

Dar iată chestia: aproape niciodată nu vrei să folosești selectori de ID pentru stilizare. Pentru că ID-urile sunt unice, nu poți reutiliza stilizarea. Și ID-urile au specificitate mare, ceea ce face greu să le suprascrii mai târziu.

Folosește id pentru link-uri de ancoră și JavaScript. Folosește clase pentru stilizare.

Regula de internalizat

Întinde mâna la o clasă 99% din timp. Selectori de element pentru implicituri globale. ID-uri pentru identificare, nu stilizare.

Developerii mai noi sunt tentați să folosească ID-uri pentru stilizare pentru că par „mai specifice" sau „mai semantice" pentru elemente unice. E o capcană. Folosește mereu clase.

Combinarea selectorilor

Mai multe clase pe un element

<article class="card featured">...</article>
.card.featured {
  border: 2px solid gold;
}

.card.featured — fără spațiu — țintește elemente care au ambele clase.

Selector descendent (spațiu)

article p {
  font-size: 18px;
}

Asta țintește fiecare <p> care e înăuntrul unui <article> — la orice adâncime.

Selector copil direct (>)

article > p {
  font-size: 18px;
}

> înseamnă „copil direct" — doar paragrafe care sunt copii imediați ai unui article, nu imbricate mai adânc.

Grupare cu virgule

h1, h2, h3, h4 {
  font-family: 'Instrument Serif', Georgia, serif;
}

Fiecare h1, h2, h3 și h4 primește același font.

Exemplu explicat
Un pattern real de stilizare
<section class="produse">
  <article class="produs">
    <h2 class="produs-titlu">Tastatură</h2>
    <p class="produs-pret">€49</p>
  </article>

  <article class="produs featured">
    <h2 class="produs-titlu">Tastatură Pro</h2>
    <p class="produs-pret">€89</p>
  </article>
</section>
.produs {
  padding: 20px;
  border: 1px solid #ddd;
}

.produs.featured {
  border-color: gold;
}

.produs-titlu {
  font-size: 20px;
  margin: 0 0 8px;
}
Numele claselor descriu ce este fiecare lucru. .produs-titlu e titlul unui produs. Claritatea aia face CSS-ul să fie auto-documentat.

Specificitate — versiunea simplă

Când mai multe reguli se aplică aceluiași element, care câștigă? Asta e specificitatea.

  • Stiluri inline bat totul
  • ID-urile bat clasele
  • Clasele bat selectorii de element
  • Selectorii de element sunt cei mai slabi
  • Regulile mai târzii bat regulile mai devreme dacă specificitatea e egală
Nu te bloca pe specificitate

Începătorii pierd adesea ore luptându-se cu specificitatea. Sfatul profesional: evită selectori cu specificitate mare în primul rând. Stai la clase, ține selectorii scurți și rar vei avea nevoie să te gândești la specificitate.

Alege selectorul potrivit
Ai multe card-uri de produse pe pagină, unele marcate ca „la reducere". Vrei ca toate card-urile la reducere să aibă un border roșu. Ce selector e cel mai bun?
Recapitulare
  • Selectori de element (h1, p) pentru implicituri globale
  • Selectori de clasă (.button, .card) pentru stiluri reutilizabile — folosește-i cel mai mult
  • Selectori de ID (#hero) rar; preferă clase pentru stilizare
  • Combină cu spațiu (descendent), > (copil direct), , (grupare)
  • Specificitate: inline > id > clasă > element. Păstrează selectorii simpli.
Modulul 2 · Lecția 311 min citire

Culori și tipografie

Culoarea și tipografia sunt cele două lucruri pe care utilizatorii le observă înainte de orice altceva. Le faci bine și pagina se simte proiectată. Le faci greșit și niciun layout polish nu te va salva.

Culori în CSS — cinci moduri de a le scrie

Culori cu nume

.titlu {
  color: red;
}

150+ culori au nume: red, blue, tomato, rebeccapurple. Rar folosite în muncă reală. Ok pentru prototipuri rapide.

Culori hex

.titlu {
  color: #c8553d;
}

Șase cifre hex: două pentru roșu, două pentru verde, două pentru albastru. Fiecare pereche merge de la 00 (deloc) la ff (maxim). #c8553d e un roșu-maroniu cald.

Hex e cel mai comun format în livrabilele designerilor.

RGB

.titlu {
  color: rgb(200, 85, 61);
}

Trei numere de la 0-255 pentru roșu, verde, albastru. Cu alpha (transparență):

.overlay {
  background: rgb(0, 0, 0, 0.5); /* negru 50% transparent */
}

HSL

.titlu {
  color: hsl(10, 57%, 51%);
}

Hue (0-360, o roată de culori), Saturation (0-100%, gri la viu), Lightness (0-100%, negru la alb). Aceeași culoare, mod diferit de gândit.

HSL e mai ușor de ajustat manual. Vrei o versiune puțin mai întunecată? Scade lightness. Hex și RGB nu te lasă să raționezi despre culoare așa.

OKLCH — alegerea modernă

.titlu {
  color: oklch(56% 0.11 35);
}

Idee similară cu HSL dar perceptual uniformă. Înseamnă: dacă crești lightness cu 10%, culoarea chiar pare cu 10% mai luminoasă pentru ochi.

OKLCH e viitorul pentru sisteme de design. Orice browser modern îl suportă.

Proprietăți de tipografie

font-family

Setează tipografia. Oferă mereu fallback-uri în caz că prima alegere nu e disponibilă.

body {
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
}

Citește de la stânga la dreapta: încearcă „Inter Tight" primul. Dacă nu e disponibil, încearcă -apple-system. Apoi BlinkMacSystemFont. În final, sans-serif (fallback-ul generic).

font-size

body {
  font-size: 16px;
}

h1 {
  font-size: 48px;
}

font-weight

Valorile merg de la 100 (subțire) la 900 (gros). Cele comune:

  • 400 — normal (implicit)
  • 500 — mediu
  • 700 — bold

line-height

body {
  line-height: 1.6;
}

h1 {
  line-height: 1.1;
}

Folosește numere fără unitate.

Reguli generale:

  • Text body: 1.5-1.7
  • Titluri: 1.0-1.3 (mai strâns)
Exemplu explicat
Un setup tipografic plăcut de citit
body {
  font-family: 'Inter Tight', sans-serif;
  font-size: 17px;
  line-height: 1.65;
  color: #1a1814;
}

h1, h2, h3 {
  font-family: 'Instrument Serif', Georgia, serif;
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 400;
}

h1 { font-size: 48px; }
h2 { font-size: 32px; }
h3 { font-size: 24px; }

p {
  max-width: 65ch;
  margin-bottom: 1em;
}

a {
  color: #c8553d;
  text-decoration: underline;
  text-underline-offset: 2px;
}
Font diferit pentru titluri vs body (împerechere clasică serif/sans). Line-height generos pe body pentru citire confortabilă. Line-height strâns pe titluri pentru că sunt scurte. max-width: 65ch pe paragrafe — 65 de caractere e lățimea ideală pentru citit.

Încărcarea de fonturi din Google Fonts

Browserele vin cu câteva fonturi implicite. Pentru orice altceva, încarci fonturi de la un serviciu ca Google Fonts.

  1. Mergi la fonts.google.com și alegi fonturi
  2. Dai click pe „Get embed code"
  3. Copiezi tag-ul <link>, îl lipești în <head>-ul HTML
  4. Folosești numele fontului în CSS
Nu încărca 8 fonturi

E tentant să navighezi pe Google Fonts și să încarci 5 tipografii pentru că toate arată cool. Nu o face. Fiecare fișier de font e bytes în plus pe care utilizatorul trebuie să-i descarce.

Un design curat folosește de obicei unul sau două fonturi, maxim. Unul pentru titluri, unul pentru body.

Alege line-height-ul potrivit
Stilizezi un articol blog lung cu text body de 18px. Ce line-height e cel mai potrivit pentru paragrafe?
Recapitulare
  • Culori: hex (#c8553d), rgb, hsl sau oklch — alege una și rămâi cu ea
  • font-family cu fallback-uri: 'Fontul tău', -apple-system, sans-serif
  • font-weight pe scala 100-900; 400 e normal, 700 e bold
  • line-height fără unitate, 1.5-1.7 pentru body, 1.0-1.3 pentru titluri
  • Google Fonts pentru tipografii personalizate; încarcă doar ce ai nevoie
Modulul 2 · Lecția 49 min citire

Variabile CSS

Imaginează-ți că ai scris 500 de linii de CSS folosind culoarea de brand #c8553d în 40 de locuri diferite. Acum echipa de marketing zice „hai să ajustăm puțin roșul de brand".

Fără variabile, faci search-and-replace în 40 de locuri și te rogi să le fi prins pe toate. Cu variabile, schimbi o singură linie.

Sintaxa

Definești o variabilă cu --nume, o folosești cu var(--nume).

:root {
  --brand-rosu: #c8553d;
  --brand-verde: #4a7c59;
  --text-principal: #1a1814;
}

h1 {
  color: var(--brand-rosu);
}

.button {
  background: var(--brand-rosu);
  color: white;
}

.success {
  color: var(--brand-verde);
}

Acum var(--brand-rosu) apare oriunde vrei culoarea de brand. Schimbi valoarea o dată la :root, fiecare utilizare se actualizează.

Două lucruri de observat:

  • :root e un selector special care se potrivește cu elementul <html>. E unde pui variabilele care ar trebui să fie disponibile pentru toată pagina.
  • Numele de variabile încep cu două liniuțe (--brand-rosu). Asta e sintaxa — nu sări peste ele.

Un set tipic de design tokens

Proiectele reale definesc multe variabile la începutul stylesheet-ului. Asta se numește adesea un sistem de design sau design tokens.

:root {
  /* Culori */
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-bg: #fbfaf7;
  --color-accent: #c8553d;
  --color-success: #4a7c59;

  /* Tipografie */
  --font-sans: 'Inter Tight', -apple-system, sans-serif;
  --font-serif: 'Instrument Serif', Georgia, serif;

  /* Dimensiuni */
  --size-sm: 8px;
  --size-md: 16px;
  --size-lg: 32px;

  /* Margini */
  --radius: 8px;
  --radius-lg: 14px;
}

body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
}

.card {
  border-radius: var(--radius-lg);
  padding: var(--size-md);
}

.button {
  background: var(--color-accent);
  border-radius: var(--radius);
  padding: var(--size-sm) var(--size-md);
}
Alege nume semantice, nu descriptive

Numește variabilele după ce înseamnă, nu după cum arată.

/* Rău — se strică de îndată ce roșul devine albastru */
--rosu: #c8553d;
--gri-mediu: #888;

/* Bun — sensul e stabil */
--color-accent: #c8553d;
--color-text-muted: #888;

Când o variabilă se numește --rosu și rebranduiești la albastru, fiecare nume de variabilă e acum înșelător.

Variabilele pot conține orice

Nu doar culori. Orice valoare CSS validă poate fi o variabilă:

:root {
  --transition-rapid: 150ms ease-out;
  --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.08);
  --header-height: 64px;
  --max-width: 1200px;
}

.card {
  transition: transform var(--transition-rapid);
  box-shadow: var(--shadow-card);
}

main {
  max-width: var(--max-width);
  margin: 0 auto;
  padding-top: var(--header-height);
}

Suprascrieri locale — unde variabilele strălucesc cu adevărat

Variabilele definite la :root sunt globale. Dar poți redefini o variabilă oriunde, și se aplică doar în acel element și descendenții săi.

:root {
  --accent: #c8553d;
}

.sectiune-intunecata {
  --accent: #e8a280;
}

.button {
  background: var(--accent);
}

Clasa .button rezolvă var(--accent) în funcție de unde e folosită:

  • Înăuntrul .sectiune-intunecata, e #e8a280
  • Oriunde altundeva, e #c8553d

Așa funcționează tema. O singură clasă .button, două aspecte diferite, fără CSS duplicat.

Exemplu explicat
Mod light și dark cu un singur comutator
:root {
  --bg: #fbfaf7;
  --text: #1a1814;
  --accent: #c8553d;
}

[data-theme="dark"] {
  --bg: #1a1814;
  --text: #e8e3d5;
  --accent: #e8a280;
}

body {
  background: var(--bg);
  color: var(--text);
}

.button {
  background: var(--accent);
}
Acum, adaugă data-theme="dark" la <html> sau <body>: fiecare variabilă se schimbă deodată. Butonul, fundalul, textul, toate se comută la valorile dark.

Când folosești variabile vs valori simple

Folosește variabile când:

  • Valoarea apare în 3+ locuri
  • Valoarea face parte dintr-un sistem de design (culori, fonturi, spațiere)
  • Vei dori să o tematizi sau suprascrii mai târziu

Folosește valori simple când:

  • E o ajustare unică — ca margin-top: 4px pentru o singură ajustare vizuală
Care nume e mai bun?
Configurezi culori pentru un sistem de design. Care numire e cea mai bună?
Recapitulare
  • Definește variabile la :root cu --nume: valoare
  • Folosește-le oriunde cu var(--nume)
  • Numește după scop (--color-accent), nu după aspect (--rosu)
  • Suprascrie variabile în selectori specifici pentru tematizare
  • Folosește pentru valori repetate sau părți ale sistemului tău de design
Modulul 2 · Lecția 59 min citire

Unități și valori

CSS are multe unități. Asta e intenționat — unități diferite se potrivesc job-urilor diferite. Folosirea celei greșite creează bug-uri subtile care-ți bântuie layout-ul pe device-uri pe care nu le-ai testat.

Unități absolute — px

px e unitatea pe care o știe toată lumea. Un pixel pe ecran.

.box {
  width: 300px;
  padding: 20px;
  border: 1px solid black;
}

Folosește px pentru:

  • Border-uri (1px, 2px)
  • Lucruri care nu ar trebui să scaleze cu dimensiunea textului — iconițe, separatoare subțiri
  • Breakpoint-uri media queries

Nu folosi px pentru:

  • Dimensiunea fonturilor pe text body (nu respectă preferințele de zoom ale utilizatorului)

Unități relative — rem și em

rem — relativ la rădăcină

1rem = oricare ar fi font-size-ul elementului <html> (implicit 16px în orice browser).

h1 {
  font-size: 3rem;       /* 48px */
  margin-bottom: 1.5rem; /* 24px */
}

p {
  font-size: 1rem;       /* 16px */
  line-height: 1.5;
}

Folosește rem pentru:

  • Dimensiuni de font (respectă zoom-ul utilizatorului)
  • Spațiere între blocuri
  • Orice ar trebui să scaleze când utilizatorul crește dimensiunea fontului implicit

rem e unitatea implicită pentru majoritatea stilizărilor în proiecte profesionale.

em — relativ la părinte

1em = font-size-ul elementului curent.

.button {
  font-size: 16px;
  padding: 0.5em 1em; /* 8px vertical, 16px orizontal */
}

em e util când spațierea ar trebui să scaleze cu dimensiunea componentei. Dacă crești font-size-ul lui .button la 20px, padding-ul crește proporțional.

Capcana imbricării em

em e relativ la font-size-ul imediat al elementului. Dacă imbrici dimensiuni em, se compun:

.list {
  font-size: 1.2em;
}

.list .item {
  font-size: 1.2em; /* 1.44em din părinte! */
}

.list .item .sub-item {
  font-size: 1.2em; /* 1.728em din original! */
}

Lucrurile cresc alarmant dacă nu ești atent. rem evită asta complet pentru că e mereu relativ la <html>.

Procente — %

% e relativ la ceva dependent de context:

  • width: 50% — 50% din lățimea părintelui
  • height: 100% — 100% din înălțimea părintelui (dar părintele are nevoie de înălțime definită)
  • font-size: 120% — 120% din font-size-ul părintelui (se comportă ca em)

Unități de viewport — vw, vh

Relative la viewport-ul browserului.

  • 1vw = 1% din lățimea viewport-ului
  • 1vh = 1% din înălțimea viewport-ului
.hero {
  height: 100vh; /* umple ecranul */
}

.big-text {
  font-size: 5vw; /* scalează cu lățimea ferestrei */
}

Capcana viewport-ului mobil — dvh

Pe browserele mobile, bara URL se arată și se ascunde în timpul scroll-ului. Asta schimbă „înălțimea viewport-ului". Dacă folosești 100vh, elementul tău ar putea fi prea înalt.

CSS modern rezolvă asta cu unități de viewport dinamice:

  • 100dvh — înălțime dinamică de viewport (se ajustează când bara URL apare/dispare)
.hero {
  min-height: 100dvh; /* înălțime completă, se ajustează corect pe mobil */
}

Folosește dvh în loc de vh pentru secțiuni full-height prietenoase cu mobil. Ăsta e implicitul modern.

Unitate de caractere — ch

1ch e lățimea caracterului 0 din fontul curent.

p {
  max-width: 65ch;
}

Asta face paragrafele să fie în jur de 65 de caractere lățime — o lungime de linie optimă pentru citit, larg cercetată.

Care unitate când — un cheat sheet

  • Dimensiuni de font → rem
  • Spațiere între blocuri → rem
  • Padding component intern care scalează → em
  • Lățimi de border → px
  • Secțiuni fullscreen → dvh
  • Lățime de paragraf citibilă → ch
Alege unitatea
Setezi font-size-ul unui paragraf ca să respecte preferințele de font-size ale browser-ului utilizatorului. Care unitate e cea mai bună?
Recapitulare
  • px pentru border-uri și lucruri fixe în pixeli
  • rem pentru majoritatea dimensiunilor de font și spațiere — respectă preferințele utilizatorului
  • em pentru scalare intern-componentă
  • % pentru lățimi proporționale în părinte
  • dvh (nu vh) pentru secțiuni full-height prietenoase cu mobil
  • ch pentru lățime de citit confortabilă (~65ch pentru paragrafe)
Modulul 2 · Lecția 69 min citire

Proprietatea display

Fiecare element HTML are un mod implicit de a ocupa spațiu pe pagină. Unele elemente se stivuiesc vertical (paragrafe, div-uri, titluri). Altele curg într-o linie cu text (span, strong, a). Asta e controlat de proprietatea display.

Înțelegerea display e poarta spre înțelegerea layout-ului.

Cele trei valori clasice de display

block

Elementele block se stivuiesc vertical. Fiecare începe pe o linie nouă și ocupă lățimea completă a containerului.

Elemente block implicit: <p>, <h1>-<h6>, <div>, <section>, <article>.

Comportament block:

  • Ia lățimea completă a părintelui implicit
  • Începe pe o linie nouă
  • Respectă width, height, margin, padding pe toate părțile

inline

Elementele inline curg cu textul. Nu încep linii noi — stau în fluxul de text.

Elemente inline implicit: <span>, <strong>, <em>, <a>, <code>.

<p>
  Asta e un paragraf cu <strong>text bold</strong> și
  <a href="#">un link</a> înăuntru.
</p>

Comportament inline:

  • Ia doar atât cât e conținutul
  • Stă pe aceeași linie ca elementele inline vecine
  • width și height nu funcționează pe elementele inline

inline-block

Se comportă ca inline (stă pe aceeași linie cu textul), dar acceptă width și height ca block.

.tag {
  display: inline-block;
  width: 80px;
  padding: 4px 12px;
  background: #f5e8e3;
  border-radius: 100px;
}
Exemplu explicat
Vizualizează diferența
.box {
  background: #e8e3d5;
  padding: 10px;
  width: 120px; /* funcționează doar pe block sau inline-block */
}

.as-block { display: block; }
.as-inline { display: inline; }
.as-inline-block { display: inline-block; }
Cele două div-uri block se stivuiesc vertical, fiecare 120px. Spans-urile inline curg orizontal, dar lățimea e ignorată. Spans-urile inline-block curg orizontal ȘI respectă lățimea.

Schimbarea display

Link-ul block-level

Un pattern comun — făcând un card întreg clickabil:

<a href="/produs/1" class="produs-link">
  <h3>Tastatură</h3>
  <p>€49</p>
</a>
.produs-link {
  display: block;
  padding: 20px;
  border: 1px solid #ddd;
  text-decoration: none;
  color: inherit;
}

Acum cardul întreg (inclusiv padding-ul) e clickabil.

Listă orizontală

.nav-list {
  list-style: none;
  padding: 0;
}

.nav-list li {
  display: inline-block;
  margin-right: 20px;
}

Acum elementele curg orizontal.

display: none — complet ascuns

.menu {
  display: none;
}

Înlătură elementul din pagină complet. Nu ocupă spațiu. Nu e vizibil. Nu e citit de cititoare de ecran.

O privire la ce vine — flex și grid

CSS modern are încă două valori de display care au revoluționat layout-ul:

.flex-container {
  display: flex;
  /* Aranjează copiii într-un rând flexibil sau coloană */
}

.grid-container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  /* Aranjează copiii într-un grid 2D */
}

display: flex e Modulul 4 — vom merge adânc. display: grid e Modulul 5. Ambele vor înlocui inline-block pentru majoritatea nevoilor de layout.

Block vs inline, capcana începătorului

Developerii noi scriu adesea cod așa și se întreabă de ce nu funcționează:

span {
  width: 200px;
  height: 100px;
}

<span> e inline implicit. width și height nu se aplică. Schimbă display-ul mai întâi:

span {
  display: inline-block;
  width: 200px;
  height: 100px;
}
Ce valoare de display?
Vrei un buton personalizat care stă inline cu textul din jur dar are o lățime și padding specifice. Care valoare de display?
Recapitulare
  • block se stivuiește vertical, ia lățime completă, respectă toate dimensiunile
  • inline curge cu textul, ignoră width/height, nu cauzează line break-uri
  • inline-block combină cele două: flow inline, suport complet pentru dimensiuni
  • display: none înlătură un element din pagină complet
  • Flex și Grid vin în Modulele 4 și 5 — vor înlocui majoritatea nevoilor de layout
Modulul 2 · Lecția 77 min citire

Box-sizing și reset

Orice fișier CSS pentru restul carierei tale ar trebui să înceapă cu două lucruri: o regulă box-sizing și un mic reset. Aceste câteva linii elimină o clasă întreagă de bug-uri care confuză începătorii și încetinesc profesioniștii.

Problema box-sizing

Când setezi width: 300px pe o cutie, cât spațiu ocupă de fapt?

.box {
  width: 300px;
  padding: 20px;
  border: 2px solid black;
}

Ai putea presupune 300px. Dar implicit, răspunsul e 344px — pentru că box-sizing-ul implicit e content-box:

  • width e doar zona de conținut
  • Padding-ul se adaugă la acea lățime
  • Border-ul se adaugă și el

Deci cutia ta de 300px e: 300 (conținut) + 2×20 (padding) + 2×2 (border) = 344px.

Asta e confuz și face layout-urile greu de raționat. Setezi o lățime și cutia ta e mai lată decât ai cerut.

Soluția: box-sizing: border-box

Schimbă box-sizing la border-box și lățimea devine predictibilă.

.box {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 2px solid black;
}

Acum:

  • width e lățimea totală exterioară (inclusiv padding și border)
  • Padding-ul și border-ul cresc înspre interior
  • Cutia e exact 300px lățime, cum ai cerut

Aplică-l global

Vrei asta peste tot. Aplică-l fiecărui element prin selector universal:

*,
*::before,
*::after {
  box-sizing: border-box;
}

Pune asta la începutul fiecărui stylesheet pe care-l vei scrie vreodată. Fiecare profesionist face asta.

Un reset CSS modern

Dincolo de box-sizing, browserele au multe stiluri încorporate pe care de obicei vrei să le elimini sau normalizezi:

  • Margini implicite pe body
  • Margini pe paragrafe și titluri
  • Bullet points pe liste în navigare
  • Stiluri inconsistente de form elements între browsere

Curățarea se numește un reset. Iată unul minimalist modern:

/* Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}
Exemplu explicat
Dezambalarea reset-ului
box-sizing: border-box peste tot — deja acoperit mai sus.

margin: 0 pe tot — browserele dau margini implicite. Asta aduce totul la zero ca să începi de la capăt.

line-height: 1.5 pe body — un implicit rezonabil pentru text body lizibil.

display: block; max-width: 100% pe imagini — oprește gap-ul ciudat de jos sub imagini inline și le face responsive.

font: inherit pe form elements — implicit, butoanele și inputurile folosesc fontul sistemului, nu fontul tău body ales.

overflow-wrap: break-word pe text elements — previne cuvinte lungi (URL-uri) să spargă containerul.

Starter-ul tău CSS

Combină reset-ul cu design tokens și ai un starter gata pentru orice proiect:

/* === Reset === */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  line-height: 1.5;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

/* === Design tokens === */
:root {
  --color-text: #1a1814;
  --color-bg: #fbfaf7;
  --color-accent: #c8553d;
  --font-sans: system-ui, -apple-system, sans-serif;
}

/* === Base === */
body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
  font-size: 1rem;
}
Nu scrie reset-uri de la zero de fiecare dată

Reset-ul de mai sus e o versiune condensată a standardelor. Două celebre:

  • Eric Meyer's reset (2008) — istoric, aduce totul la zero
  • Normalize.css — normalizează diferențele între browsere fără să elimine implicite

Nu ai nevoie să le memorezi. Doar lipește reset-ul la începutul fiecărui proiect.

De ce border-box?
Ce face setarea box-sizing: border-box pe un element?
Recapitulare
  • box-sizing: content-box implicit adaugă padding/border la width (confuz)
  • box-sizing: border-box face width dimensiunea totală exterioară (predictibil)
  • Aplică-l global cu *, *::before, *::after { box-sizing: border-box; }
  • Un reset modern aduce la zero margini implicite, repară comportamentul imaginilor și moștenește fontul pe form elements
  • Fiecare proiect începe cu acest boilerplate — lipește-l la începutul stylesheet-ului
Modulul 2 · Lecția 815 min citire

Proiect: stilizează pagina Despre Mine

Ai o pagină Despre Mine solidă în HTML din Modulul 1. Ai trusa CSS din Modulul 2. Timp să le punem împreună.

Ăsta e un proiect ghidat. Te conduc prin decizii, îți arăt codul și explic raționamentul. La final, pagina ta urâtă de 1995 va arăta ca ceva ce ai pune efectiv pe internet.

Ce construim

Ia pagina Despre Mine din Modulul 1. Adaugă CSS ca să:

  • Aibă o ierarhie vizuală clară — titlurile ies în evidență, textul body se citește confortabil
  • Folosească o combinație distinctivă de fonturi (serif pentru titluri, sans-serif pentru body)
  • Aibă o paletă de culori caldă și coerentă (fără negru și albastru implicit)
  • Aibă spațiere plăcută în jurul fiecărui element

Pasul 1: Începe stylesheet-ul

Creează styles.css lângă index.html. Linkează-l din <head>:

<link rel="stylesheet" href="styles.css">

Începe cu reset-ul și design tokens:

/* === Reset === */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
body { line-height: 1.5; }
img { display: block; max-width: 100%; }
input, button, textarea { font: inherit; }

/* === Design tokens === */
:root {
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-bg: #fbfaf7;
  --color-accent: #c8553d;
  --color-border: rgba(26, 24, 20, 0.1);

  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;
}

Pasul 2: Încarcă fonturile

Adaugă în <head>:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;700&display=swap" rel="stylesheet">

Pasul 3: Stilizează body-ul

body {
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--color-text);
  background: var(--color-bg);
  max-width: 65ch;
  margin: 0 auto;
  padding: 3rem 1.5rem;
}

Câteva alegeri deliberate:

  • max-width: 65ch — ține paragrafele la lățime ideală de citit
  • margin: 0 auto — centrează body-ul orizontal
  • padding: 3rem 1.5rem — spațiu de respirat generos sus/jos

Pasul 4: Stilizează titlurile

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--color-text);
}

h1 {
  font-size: 3rem;
  margin-bottom: 0.5rem;
}

h2 {
  font-size: 2rem;
  margin-top: 3rem;
  margin-bottom: 1rem;
}

h3 {
  font-size: 1.5rem;
  margin-top: 2rem;
  margin-bottom: 0.75rem;
}

Fontul serif pentru titluri contrastează frumos cu body-ul sans-serif. Line-height strâns și letter-spacing negativ fac textul mare să se simtă intenționat.

Pasul 5: Stilizează paragrafele

p {
  margin-bottom: 1.25rem;
  color: var(--color-text);
}

p:last-child {
  margin-bottom: 0;
}

Pasul 6: Stilizează header-ul și nav-ul

header {
  margin-bottom: 3rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--color-border);
}

nav ul {
  list-style: none;
  padding: 0;
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
}

Am strecurat display: flex — un mic preview din Modulul 4.

Pasul 7: Stilizează link-urile

a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color 150ms ease;
}

a:hover {
  color: #a8432e;
}

text-underline-offset face subliniile să se simtă mai rafinate. transition face schimbarea de culoare hover smooth.

Pasul 8: Stilizează formularul

form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 400px;
}

label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-soft);
}

input, textarea {
  width: 100%;
  padding: 0.75rem 1rem;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: white;
}

input:focus, textarea:focus {
  outline: none;
  border-color: var(--color-accent);
}

button[type="submit"] {
  padding: 0.875rem 1.5rem;
  background: var(--color-text);
  color: var(--color-bg);
  border: none;
  border-radius: 8px;
  font-weight: 500;
  cursor: pointer;
  align-self: flex-start;
}

Pasul 9: Stilizează footer-ul

footer {
  margin-top: 4rem;
  padding-top: 2rem;
  border-top: 1px solid var(--color-border);
  color: var(--color-text-soft);
  font-size: 0.875rem;
}

Ce tocmai ai construit

O pagină Despre Mine stilizată care ar arăta la locul ei pe orice portofoliu modern. Ai folosit:

  • Un reset CSS (comportament consistent)
  • Design tokens (valori centralizate)
  • Împerechere tipografică (serif display + sans-serif body)
  • O paletă caldă de culori
  • Lățimi și line-height-uri confortabile pentru citit
  • Focus states pe form inputs
  • Mici tranziții hover pe link-uri și butoane
Personalizează, nu doar copia

Scopul proiectului nu e ca toată lumea să termine cu aceeași pagină. Schimbă culorile. Alege fonturi diferite. Folosește o paletă mai rece. Fă h1-ul mai mare. Fă textul body mai lat.

Skill-urile sunt în ajustat — nu în a tasta ce ți-am dat verbatim.

Recapitulare
  • Începe fiecare proiect cu un reset și design tokens
  • Împerecherea serif + sans-serif ridică sentimentul
  • Focus states, hover transitions și :last-child cleanups sunt detaliile care separă amator de pro
  • Un max-width pe body ține conținutul long-form lizibil
  • Modulul 3 îți va da control real peste cutii (padding, margini, border-uri, background-uri)

🎉 Ai terminat Modulul 2!
Ai acum fundamentele CSS. Modulul 3 (Box Model) îți dă control real peste cutii — padding, margini, backgrounds, borders.

Modulul 3 · Lecția 19 min citire

Box model-ul pe bune

În CSS, fiecare element de pe pagină e o cutie. Fără excepție. Titlurile, paragrafele, imaginile, link-urile — toate sunt cutii dreptunghiulare care ocupă spațiu.

Acele cutii au patru straturi. Odată ce le înțelegi, controlezi complet cum se aranjează lucrurile pe pagină. Asta e lecția aia.

Cele patru straturi ale unei cutii

Fiecare cutie CSS are, din interior spre exterior:

  1. Content — conținutul (textul, imaginea, copiii)
  2. Padding — spațiul dintre conținut și border
  3. Border — marginea vizuală în jurul cutiei
  4. Margin — spațiul dintre această cutie și celelalte
.card {
  /* Content — controlat de width și height */
  width: 300px;

  /* Padding — spațiu interior */
  padding: 20px;

  /* Border — linia vizibilă */
  border: 2px solid #1a1814;

  /* Margin — spațiu exterior */
  margin: 16px;
}

Fiecare strat, explicat

Content

Zona interioară unde trăiește textul sau alte elemente. Dimensiunea ei e controlată de width și height (sau lăsată implicit să se adapteze la conținut).

Padding

Spațiu între conținut și border. Crează „loc de respirat" înăuntrul cutiei.

/* Padding egal pe toate cele 4 părți */
.box { padding: 20px; }

/* Pe verticală 20px, pe orizontală 40px */
.box { padding: 20px 40px; }

/* Sus 10, dreapta 20, jos 30, stânga 40 (în sensul acelor de ceas) */
.box { padding: 10px 20px 30px 40px; }

/* Individual */
.box {
  padding-top: 10px;
  padding-right: 20px;
  padding-bottom: 30px;
  padding-left: 40px;
}

Border

Marginea vizibilă în jurul cutiei. Are 3 părți: lățime, stil, culoare.

/* Prescurtat */
.box { border: 2px solid #c8553d; }

/* Stiluri posibile */
.box { border: 2px dashed gray; }
.box { border: 2px dotted gray; }
.box { border: 3px double gray; }

/* Doar o parte */
.box { border-bottom: 1px solid gray; }

Margin

Spațiul exterior al cutiei — cât de mult stă departe de alte cutii. Aceeași sintaxă ca padding.

.box { margin: 16px; }
.box { margin: 1rem 2rem; }
.box { margin-top: 2rem; }

/* Centrare orizontală — auto pe stânga și dreapta */
.box {
  width: 600px;
  margin: 0 auto;
}
Padding vs margin — când care

Întrebare frecventă. Regula simplă:

  • Padding — pentru spațiu în interiorul cutiei (între conținut și border). Dacă ai un card, padding-ul e ce face textul să nu stea lipit de margini.
  • Margin — pentru spațiu între cutii diferite. Dacă ai două carduri unul sub altul, margin-ul e ce-i separă.

Un card are ambele: padding ca să aerisească interiorul, margin ca să-l distanțeze de alte carduri.

Vizualizare în DevTools

Deschide DevTools (F12) pe orice site, selectează un element, uită-te în tab-ul „Computed" sau „Box Model". Vei vedea o diagramă colorată cu cele 4 straturi și valorile lor curente. E cel mai bun mod să înțelegi ce se întâmplă în layout-ul tău.

Exemplu explicat
Un card complet
.card {
  /* Lățime totală cu border-box */
  box-sizing: border-box;
  width: 320px;

  /* Interior */
  padding: 24px;

  /* Margine vizuală subtilă */
  border: 1px solid rgba(0,0,0,0.1);
  border-radius: 12px;

  /* Fundal ca să iasă în evidență */
  background: white;

  /* Spațiu între el și alte carduri */
  margin-bottom: 16px;
}
Cu box-sizing: border-box (din Modulul 2), cardul e fix 320px lățime — padding-ul crește înspre interior. Fără asta, 320 + 2×24 + 2×1 = 370px total. Predictibilitatea contează.

Valori negative pentru margin

Spre deosebire de padding, margin poate fi negativ. Asta trage elementul mai aproape de vecini sau chiar peste ei.

.overlapping-card {
  margin-top: -20px; /* se suprapune cu cardul de deasupra */
}

Rar folosit, dar ocazional util pentru design-uri unde elementele se suprapun vizual.

Alege corect
Ai un card cu text înăuntru. Textul pare lipit de marginea cardului — vrei să respire mai mult în interior. Ce proprietate ajustezi?
Recapitulare
  • Fiecare element e o cutie cu 4 straturi: content, padding, border, margin
  • Padding — spațiu interior (între conținut și border)
  • Margin — spațiu exterior (între cutii)
  • Border — marginea vizuală
  • Folosește padding: 10px 20px 30px 40px pentru 4 valori (sus, dreapta, jos, stânga)
  • Margin poate fi negativ, padding nu
Modulul 3 · Lecția 28 min citire

Width, height și limite

Părerea că „lățimea e doar un număr" ține până ajungi la ecrane mici, conținut lung sau imagini ciudate. CSS are câteva unelte dincolo de simplul width care fac layout-urile să funcționeze în situații reale.

Comportamentul implicit

Înainte să setezi vreo lățime manual, reține ce fac elementele implicit:

  • Elementele block (div, p, h1, section) se întind automat pe lățimea părintelui
  • Elementele inline (span, a, strong) iau doar atât cât e conținutul
  • Imaginile au lățimea intrinsecă a fișierului

De cele mai multe ori, comportamentul implicit e exact ce vrei. Intervenim cu width când vrem altceva.

Width și height de bază

.card {
  width: 320px;
  height: 200px;
}

Cu box-sizing: border-box, asta setează lățimea totală (inclusiv padding și border).

Valorile pot fi în orice unitate: px, %, rem, vw, etc.

Min-width și max-width

Mai utile decât width fix pentru design responsive.

.container {
  width: 90%;
  max-width: 1200px; /* nu depăși 1200px chiar dacă 90% e mai mult */
}

.card {
  width: 100%;
  min-width: 280px; /* niciodată mai îngust de 280px */
  max-width: 400px; /* niciodată mai lat de 400px */
}

De ce e asta important:

  • max-width pe un container previne să devină prea lat pe ecrane mari
  • min-width previne elementele să devină prea înguste și ilizibile
  • Combinația cu width: 100% îți dă un element care se întinde în limite sigure

Înălțimea e mai complicată

Spre deosebire de lățime, înălțimea e de obicei lăsată automată. Setez rareori o înălțime fixă pe un element care conține text.

/* Problematic — dacă textul crește, se taie sau iese */
.card { height: 200px; }

/* Mai bine — crește cu conținutul, dar nu mai jos de 200px */
.card { min-height: 200px; }
Regula aurie: min-height, nu height

Pentru elemente cu text variabil (carduri, celule, secțiuni), folosește min-height în loc de height.

Motivul: height: 200px e rigid. Dacă utilizatorul mărește dimensiunea fontului, sau tu adaugi text, conținutul fie se taie, fie iese în afară. min-height: 200px garantează un minim dar permite cutiei să crească când e nevoie.

Height fixă doar pentru elemente decorative (iconițe, separatoare) unde dimensiunea nu trebuie să se schimbe.

Pattern-uri comune de lățime

Container centrat cu lățime maximă

.container {
  max-width: 1200px;
  margin: 0 auto; /* centrează orizontal */
  padding: 0 1.5rem; /* spațiu pe margini pe ecrane mici */
}

Cel mai folosit pattern de layout pe web. Content-ul stă frumos la mijloc pe ecrane mari și umple ecranul pe cele mici.

Secțiune full-width cu content centrat

section {
  width: 100%; /* toată lățimea */
  background: #f4f2ec; /* fundal plin până la margini */
}

section .inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 4rem 1.5rem;
}

Secțiunea întinde fundalul peste tot ecranul, dar conținutul rămâne centrat la lățime citibilă.

Text cu lățime optimă de citit

article p {
  max-width: 65ch;
}

65 de caractere e lățimea optimă pentru citit — conform cercetărilor în tipografie.

Exemplu explicat
Layout tipic de blog
body {
  /* body-ul n-are lățime setată — implicit ia tot ecranul */
}

.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

article {
  max-width: 65ch;
  margin: 0 auto; /* centrat în interiorul .page */
}

article img {
  width: 100%; /* umple lățimea articolului */
  max-width: 100%; /* nu crește peste asta */
  height: auto; /* păstrează proporțiile */
}
Container-ul .page limitează lățimea pe desktop. Articolul se îngustează și mai mult pentru citit confortabil. Imaginile umplu articolul dar păstrează proporțiile naturale.
Layout responsive
Vrei un container care ocupă tot ecranul pe telefoane dar nu depășește 1200px pe monitoare mari. Ce setări sunt corecte?
margin: 0 auto îl centrează pe ecrane mari. width: 1200px l-ar forța să fie exact 1200px mereu — cauzând scroll orizontal pe telefon." data-msg-wrong="Nu chiar. Răspunsul corect e B. max-width setează o limită superioară, lăsând elementul să fie mai îngust când e nevoie. width l-ar forța rigid.">
Recapitulare
  • Elementele block iau lățimea completă implicit, inline iau cât e conținutul
  • Folosește max-width în loc de width pentru containere responsive
  • Folosește min-height în loc de height pentru elemente cu text
  • Pattern container centrat: max-width + margin: 0 auto
  • Pentru text lizibil: max-width: 65ch
Modulul 3 · Lecția 38 min citire

Margini și margin collapse

Margins sunt simple — setezi un spațiu, primești un spațiu. Până când două margini se întâlnesc și se comportă ciudat. Bună vedere la margin collapse, fenomenul CSS care confuză developerii de 25 de ani.

Înveți regulile odată, nu te mai prinde niciodată.

Repetarea rapidă a margin-urilor

/* Toate 4 părțile */
.box { margin: 16px; }

/* Vertical / orizontal */
.box { margin: 20px 40px; }

/* Fiecare parte */
.box { margin: 10px 20px 30px 40px; } /* sus, dreapta, jos, stânga */

/* Individual */
.box {
  margin-top: 2rem;
  margin-bottom: 1rem;
}

Margin collapse — ce e

Când două elemente block adiacente au margini verticale, acestea se colapsează în una singură — cea mai mare dintre ele.

.de-sus { margin-bottom: 30px; }
.de-jos { margin-top: 20px; }

Ai crede că spațiul între ele e 30 + 20 = 50px. De fapt e doar 30px. Cea mai mare câștigă, cealaltă e ignorată.

De ce se întâmplă asta

E o moștenire din epoca documentelor tipărite — când ai un paragraf cu margin jos de 20px și următorul cu margin sus de 20px, nu vrei 40px între ele. Vrei un spațiu consistent.

CSS a păstrat comportamentul, chiar dacă e contraintuitiv pentru dev-eri noi. Acceptă-l și mergi mai departe.

Când se colapsează margin-urile

Doar în situații verticale și block:

  • Între două elemente block adiacente (paragraf după paragraf)
  • Între un părinte și primul/ultimul copil al său
  • Când un element gol are margin sus și jos

Când NU se colapsează

  • Margini orizontale (stânga/dreapta) niciodată nu se colapsează
  • Padding rupe colapsarea — dacă părintele are padding, nu colapsează cu copilul
  • Border rupe colapsarea — același lucru
  • Flex și Grid containers — nu se aplică deloc margin collapse pe copii

Pattern: niciodată margin-top pe primul, niciodată margin-bottom pe ultimul

O convenție pe care o adoptă majoritatea dezvoltatorilor pentru a evita confuzii:

/* Folosește doar margin-bottom pe elemente */
h2 { margin-bottom: 1rem; }
p { margin-bottom: 1rem; }
ul { margin-bottom: 1rem; }

/* Niciodată margin-top — evită colapsări surprinzătoare */

Sau inversul — doar margin-top. Important e să fii consistent într-un proiect.

Soluția modernă: gap cu flex și grid

Dacă folosești Flexbox sau Grid (module viitoare), proprietatea gap elimină toate problemele de margin collapse:

.lista-carduri {
  display: flex;
  flex-direction: column;
  gap: 1rem; /* spațiu consistent între carduri, fără drama margin-ului */
}

Ăsta e modul modern de a gândi spațierea. Vei folosi gap în fiecare proiect de aici încolo.

Exemplu explicat
Vezi diferența cu/fără padding pe părinte
/* Margin-ul h2 colapsează cu părintele */
section {
  background: yellow;
}
section h2 {
  margin-top: 30px;
  /* În loc să creeze spațiu sus în section,
     colapsează în afara lui și pushează section-ul întreg */
}

/* Cu padding, colapsarea e blocată */
section {
  padding-top: 1px; /* un pixel e suficient să rupă */
  background: yellow;
}
section h2 {
  margin-top: 30px;
  /* Acum margin-ul e în interiorul section-ului */
}
Un truc pro: adaugă padding: 1px sau overflow: hidden pe un container care „mănâncă" margin-urile copiilor.

Margin: auto — centrat cu magie

Setând margin: 0 auto (sau margin-left: auto; margin-right: auto) centrezi un element block orizontal. Browser-ul distribuie spațiul rămas egal între stânga și dreapta.

.container {
  max-width: 1200px;
  margin: 0 auto; /* centrat */
}

Notă: nu merge pentru centrare verticală (margin: auto 0 nu face nimic pe block). Pentru verticală, folosește Flexbox (Modul 4).

Rezolvă colapsarea
Ai două paragrafe unul sub altul. Primul are margin-bottom: 24px, al doilea margin-top: 16px. Ce spațiu apare între ele?
Recapitulare
  • Margin collapse: margini verticale adiacente se combină — cea mai mare câștigă
  • Se întâmplă doar pe vertical, doar între elemente block
  • Nu se întâmplă în Flex sau Grid containers
  • Convenție: folosește doar margin-bottom sau doar margin-top, nu amândouă
  • Modern: folosește gap în Flex/Grid și evită problema complet
  • margin: 0 auto centrează block orizontal
Modulul 3 · Lecția 49 min citire

Background-uri

Fiecare element poate avea un fundal. O culoare plină, o imagine, un gradient, sau combinații între toate trei. Background-urile bune fac pagina să pară proiectată cu gust. Background-urile proaste o fac să arate ca un site de spam din 2005.

Background color

Cel mai simplu — o culoare plină.

.card {
  background: white;
}

.hero {
  background: #1a1814;
}

.alert {
  background: rgba(200, 85, 61, 0.1); /* roșu cu transparență */
}

Poți folosi background sau background-color. background e mai scurt și permite combinații (culoare + imagine + gradient).

Background image

.hero {
  background-image: url('hero.jpg');
  background-size: cover; /* acoperă tot elementul */
  background-position: center; /* centrat */
  background-repeat: no-repeat; /* nu repeta */

  height: 400px;
}

Sau prescurtat:

.hero {
  background: url('hero.jpg') center / cover no-repeat;
  height: 400px;
}

Ordinea în prescurtat: imagine, poziție / dimensiune, repeat.

Proprietățile background-ului

background-size

  • cover — imaginea acoperă tot elementul, poate fi tăiată (cel mai folosit)
  • contain — imaginea încape toată, poate lăsa spațiu gol
  • 100% 100% — deformează imaginea să umple exact
  • 400px 200px — dimensiuni explicite

background-position

  • center — centrat pe ambele axe
  • top left, bottom right, etc. — colțuri
  • center bottom — poziții combinate
  • 50% 75% — procente exacte

background-repeat

  • no-repeat — o singură dată (cel mai folosit)
  • repeat — pe ambele axe (implicit)
  • repeat-x / repeat-y — doar pe o axă

Gradient-uri

Gradient-urile sunt imagini generate de CSS — tratate la fel ca background-image.

Linear gradient

.hero {
  background: linear-gradient(to bottom, #fbfaf7, #f4f2ec);
}

/* Cu unghi specific */
.card {
  background: linear-gradient(135deg, #c8553d, #6b2d1e);
}

/* Mai multe opriri */
.rainbow {
  background: linear-gradient(90deg, red, orange, yellow, green, blue, purple);
}

Radial gradient

.spotlight {
  background: radial-gradient(circle at center, #f5e8e3, #fbfaf7);
}

/* Poziționat în colț */
.corner-light {
  background: radial-gradient(circle at top right, #c8553d 0%, transparent 50%);
}
Gradient-urile subtile bat gradient-urile dramatic

Gradient-urile între două culori foarte diferite arată ieftin. Gradient-urile subtile — între două nuanțe apropiate — arată rafinate.

/* Cam ieftin */
background: linear-gradient(red, blue);

/* Rafinat */
background: linear-gradient(#fbfaf7, #f0ede4);
background: linear-gradient(135deg, rgba(200, 85, 61, 0.05), transparent);

Design-ul modern folosește gradient-uri pentru ambianță, nu efect. Dacă cineva observă gradient-ul, probabil e prea vizibil.

Combinații — mai multe fundaluri stivuite

Un element poate avea mai multe fundaluri stivuite, separate prin virgulă:

.hero {
  background:
    linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), /* strat întunecat */
    url('hero.jpg') center / cover; /* imagine dedesubt */
}

Primul e deasupra, ultimul e dedesubt. Util pentru overlay-uri de culoare peste imagini — textul devine lizibil.

Exemplu explicat
Hero cu imagine și text lizibil
.hero {
  background:
    linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0.2),
      rgba(0, 0, 0, 0.6)
    ),
    url('peisaj.jpg') center / cover no-repeat;

  min-height: 500px;
  padding: 4rem 2rem;
  color: white;
}
Gradient-ul se întunecă de sus în jos peste imagine, făcând textul alb lizibil. Fără overlay, textul poate să dispară pe zonele luminoase ale imaginii.

Background attachment

Mai rar folosit dar util pentru efecte parallax:

.parallax {
  background: url('peisaj.jpg') center / cover;
  background-attachment: fixed; /* stă pe loc când scrollezi */
}

Atenție: background-attachment: fixed poate fi lent pe mobil. Testează pe telefon înainte să-l folosești în producție.

Alege setarea potrivită
Vrei o imagine de fundal care umple tot ecranul, se adaptează la orice dimensiune, și nu se taie ciudat. Ce setări folosești?
Recapitulare
  • background acceptă culoare, imagine, gradient — sau toate combinate
  • cover = acoperă tot (poate tăia); contain = încape tot (poate lăsa goluri)
  • Gradient-urile sunt tratate ca imagini: linear-gradient, radial-gradient
  • Pot fi stivuite mai multe fundaluri separate prin virgulă
  • Overlay peste imagine = gradient semi-transparent deasupra pentru lizibilitate
Modulul 3 · Lecția 58 min citire

Border-radius și shadows

Colțurile rotunjite și umbrele subtile sunt printre cele mai mici detalii care fac diferența între un design amator și unul matur. Ambele sunt simple — dar se folosesc greșit mai des decât oricare alte proprietăți CSS.

Border-radius — colțuri rotunjite

/* Toate 4 colțurile la fel */
.card { border-radius: 8px; }

/* Diferite pe fiecare colț */
.card { border-radius: 8px 16px 8px 16px; } /* sus-stg, sus-dr, jos-dr, jos-stg */

/* Individual */
.card {
  border-top-left-radius: 8px;
  border-bottom-right-radius: 8px;
}

Valori comune

  • 4px — rotunjire foarte subtilă, elemente tehnice
  • 8px — standard pentru butoane, inputs
  • 12-16px — carduri moderne
  • 24px — elemente mari, buton gras
  • 50% — cerc perfect (pentru elemente pătrate)
  • 9999px — „pill" (capsulă) pentru elemente nepătrate

Cercuri și capsule

/* Avatar — element pătrat → cerc perfect */
.avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
}

/* Buton capsulă — indiferent de lățime */
.pill-button {
  padding: 8px 24px;
  border-radius: 9999px; /* valoare mare = capsulă */
}
Radius consistent într-un design system

Nu amesteca la întâmplare 4px, 6px, 10px, 15px prin proiect. Alege 3-4 valori și rămâi la ele.

:root {
  --radius-sm: 6px;   /* tag-uri, iconițe */
  --radius: 10px;     /* butoane, inputs */
  --radius-lg: 14px;  /* carduri */
  --radius-xl: 20px;  /* panouri mari */
}

Consistența face design-ul să pară gândit, nu întâmplător.

Box-shadow — umbre

Sintaxa de bază:

.card {
  box-shadow: [x-offset] [y-offset] [blur] [spread] [color];
}

/* Exemplu */
.card {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
  • x-offset — deplasare orizontală (negativă = stânga)
  • y-offset — deplasare verticală (pozitivă = jos)
  • blur — cât de tare e neclară umbra
  • spread — cât de mult se extinde umbra (opțional)
  • color — de obicei negru cu transparență

Rețete de umbre bune

/* Subtilă — pentru carduri pe fundal deschis */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);

/* Medie — pentru elemente hover, dropdowns */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

/* Pronunțată — pentru modale */
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);

/* Mai multe straturi — cel mai realistic */
box-shadow:
  0 1px 2px rgba(0, 0, 0, 0.04),
  0 4px 12px rgba(0, 0, 0, 0.08),
  0 20px 40px rgba(0, 0, 0, 0.12);

Ultimul pattern — 3 straturi de umbră — imită cum se comportă lumina în realitate. O umbră aproape pentru apropiere, una medie pentru distanță, una mare pentru atmosferă. Profesioniștii îl folosesc constant.

Reguli de umbre
  • Umbrele doar în jos. Y-offset pozitiv (2, 4, 8). Lumina vine de sus în design UI — umbrele merg spre podea.
  • Opacitate mică. Rar mai mult de 0.15 alfa. Umbrele prea închise arată ieftin.
  • Blur mare, spread mic. Umbrele reale sunt difuze, nu ascuțite.
  • Niciodată umbre colorate aleator. Doar negru cu transparență. Excepție: umbră în culoarea elementului pentru efect „glow".

Inset — umbre interioare

Cu inset, umbra e în interiorul elementului:

.button:active {
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
  /* Efect de apăsat */
}

.input:focus {
  box-shadow: inset 0 0 0 3px rgba(200, 85, 61, 0.1);
  /* Ring subtil în interior */
}

Text-shadow — umbre pe text

Similar ca box-shadow, dar pentru text:

h1 {
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

Folosit rar în design modern — tipografia clară pe fundal curat e mai eficientă. Util uneori pentru text peste imagini.

Exemplu explicat
Card profesional cu detalii
.card {
  background: white;
  border-radius: 14px;
  padding: 24px;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.06);
  transition: transform 150ms ease, box-shadow 150ms ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.05),
    0 12px 24px rgba(0, 0, 0, 0.08);
}
Umbra subtilă implicită + umbră mai puternică la hover + o mică ridicare (translateY). Trei detalii simple care fac cardul să pară „viu".
Care e umbra mai bună?
Pentru un card subtil pe fundal alb, care box-shadow arată cel mai profesional?
Recapitulare
  • border-radius: 8px standard, 50% cerc, 9999px capsulă
  • Alege 3-4 valori de radius și fii consistent
  • box-shadow: x y blur spread color
  • Reguli de umbre: jos, opacitate mică (≤0.15), blur mare, doar negru
  • Umbrele stratificate (3 rânduri) imită lumina reală și arată cel mai premium
Modulul 3 · Lecția 67 min citire

Overflow

Ce se întâmplă când conținutul e mai mare decât containerul? Răspunsul depinde de setarea overflow. E una dintre proprietățile cele mai ignorate de începători și cele mai importante pentru layout-uri solide.

Situația

Imaginează-ți un card de 200px înălțime care conține un text de 500px. Ce se întâmplă cu cei 300px în plus?

Implicit, textul iese din card și e vizibil în afara lui. Poate arăta ciudat, poate suprascrie alte elemente. Uneori e ce vrei, alteori nu.

Cele patru valori

overflow: visible (implicit)

Conținutul iese din container, e vizibil în afara lui. Implicit pentru toate elementele.

overflow: hidden

Conținutul care depășește e tăiat — nu se vede, nu se poate derula.

.card {
  height: 200px;
  overflow: hidden;
  /* Textul mai lung de 200px e invizibil */
}

Util când vrei margini curate — de exemplu, într-un card cu imagine și border-radius, fără overflow: hidden pe container, imaginea ar ieși din colțurile rotunjite.

overflow: scroll

Întotdeauna afișează bare de scroll, chiar dacă nu e nevoie. Rareori ce vrei.

overflow: auto

Browser-ul decide: dacă conținutul e mai mare, adaugă bare de scroll. Dacă încape, nu. Ăsta e cel pe care-l vei folosi.

.scroll-container {
  max-height: 400px;
  overflow: auto;
  /* Barele de scroll apar doar când e necesar */
}

Overflow pe o singură axă

/* Doar vertical */
.modal-content {
  max-height: 80vh;
  overflow-y: auto;
  overflow-x: hidden; /* fără scroll orizontal */
}

/* Doar orizontal (ex: un tabel mare) */
.table-wrapper {
  overflow-x: auto;
}

Pattern-uri comune

Imagine într-un card cu colțuri rotunjite

.card {
  border-radius: 14px;
  overflow: hidden; /* taie imaginea la colțurile rotunjite */
}

.card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

Fără overflow: hidden, imaginea s-ar vedea pătrată la colțurile cardului.

Text pe o linie cu „..."

.card-title {
  white-space: nowrap;      /* pe o singură linie */
  overflow: hidden;          /* ce iese se taie */
  text-overflow: ellipsis;   /* înlocuiește tăierea cu "..." */
}

Pattern ultra-folosit pentru titluri lungi în carduri, nume lungi în tabele, etc.

Text limitat la N linii

.card-desc {
  display: -webkit-box;
  -webkit-line-clamp: 3;     /* maxim 3 linii */
  -webkit-box-orient: vertical;
  overflow: hidden;
}

Taie textul la 3 linii și adaugă „..." automat. Necesar pentru carduri cu descrieri uniforme.

Modal scrollabil

.modal {
  max-height: 80vh;
  overflow-y: auto;
}

Dacă conținutul modalului e mic, nu apar bare. Dacă e mare, scrollezi în interiorul modalului fără să afectezi pagina.

Overflow hidden pe părinte rezolvă margin collapse

Un efect secundar util: overflow: hidden (sau auto) pe un container oprește margin collapse dintre el și copiii săi.

Dacă te lupți cu margini care ies în afara unui container colorat, adaugă overflow: hidden pe container. Problemă rezolvată.

Exemplu explicat
Card cu imagine, titlu trunchiat și descriere pe 3 linii
.card {
  background: white;
  border-radius: 14px;
  overflow: hidden; /* colțuri rotunjite curate */
}

.card img {
  width: 100%;
  height: 180px;
  object-fit: cover;
  display: block;
}

.card-body {
  padding: 16px;
}

.card-title {
  font-size: 18px;
  font-weight: 500;
  margin-bottom: 8px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.card-desc {
  color: #5c564b;
  font-size: 14px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
Trei utilizări de overflow într-un singur card: tăierea imaginii la colțuri, titlu pe o linie cu „...", descriere pe maxim 3 linii. Toate indiferent de conținutul real — cardul arată consistent.
Scenariu comun
Ai un sidebar cu o listă lungă de nav-items. Vrei să scrollezi în interiorul sidebar-ului dacă lista depășește înălțimea ecranului. Ce setezi?
Recapitulare
  • visible (implicit) — conținutul iese din container
  • hidden — conținutul care depășește se taie
  • auto — scroll apare doar când e necesar (cel mai folosit)
  • overflow-x și overflow-y pentru axe separate
  • Pattern-uri: text-overflow: ellipsis, -webkit-line-clamp, modal scrollabil
  • overflow: hidden rupe margin collapse — truc util
Modulul 3 · Lecția 714 min citire

Mini-proiect: galerie de carduri

Ai tot ce-ți trebuie acum să construiești componente reale. Hai să facem o galerie de carduri — tipul de componentă pe care o vezi pe orice site de e-commerce, portofoliu sau blog.

Fără Flexbox sau Grid încă (acelea vin în modulele următoare). Doar ce ai învățat. Vei fi surprins cât de departe ajungem.

Ce construim

O pagină cu un set de carduri de produse. Fiecare card are: imagine, titlu, descriere scurtă, preț, buton. Arată profesional, funcționează pe orice ecran, folosește doar HTML și CSS din modulele 1-3.

Pasul 1: HTML-ul

<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Galerie</title>
  <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;700&family=Instrument+Serif&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <h1>Produsele noastre</h1>
    <p class="lead">Selecție curată de obiecte frumoase.</p>

    <section class="galerie">
      <article class="card">
        <img src="produs1.jpg" alt="Produs 1" class="card-img">
        <div class="card-body">
          <h2 class="card-title">Tastatură mecanică</h2>
          <p class="card-desc">Switch-uri Cherry MX Brown, iluminare RGB, construcție din aluminiu.</p>
          <div class="card-footer">
            <span class="card-price">€149</span>
            <button class="card-btn">Cumpără</button>
          </div>
        </div>
      </article>
      <!-- mai multe carduri la fel -->
    </section>
  </main>
</body>
</html>

Pasul 2: Reset și design tokens

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
body { line-height: 1.5; }
img { display: block; max-width: 100%; }
button { font: inherit; cursor: pointer; border: none; }

/* Tokens */
:root {
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-text-muted: #8a8578;
  --color-bg: #fbfaf7;
  --color-panel: #ffffff;
  --color-accent: #c8553d;
  --color-accent-hover: #a8432e;
  --color-border: rgba(0, 0, 0, 0.08);

  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;

  --radius: 10px;
  --radius-lg: 14px;
}

body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
}

Pasul 3: Container și header

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 3rem 1.5rem;
}

h1 {
  font-family: var(--font-display);
  font-size: 3rem;
  font-weight: 400;
  letter-spacing: -0.02em;
  margin-bottom: 0.5rem;
}

.lead {
  font-size: 1.125rem;
  color: var(--color-text-soft);
  margin-bottom: 3rem;
  max-width: 65ch;
}

Pasul 4: Cardul

.card {
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  overflow: hidden; /* taie imaginea la colțuri */
  max-width: 320px;
  margin-bottom: 1.5rem;

  /* Umbra stratificată */
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.06);

  transition: transform 200ms ease, box-shadow 200ms ease;
}

.card:hover {
  transform: translateY(-3px);
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.05),
    0 12px 24px rgba(0, 0, 0, 0.1);
}

.card-img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  background: var(--color-bg); /* fallback dacă imaginea lipsește */
}

.card-body {
  padding: 1.25rem 1.5rem 1.5rem;
}

.card-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  margin-bottom: 0.5rem;
  line-height: 1.2;

  /* Trunchiere pe 2 linii */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-desc {
  color: var(--color-text-soft);
  font-size: 0.9375rem;
  margin-bottom: 1.25rem;

  /* Trunchiere pe 3 linii */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-footer {
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.card-price {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  color: var(--color-text);
}

.card-btn {
  padding: 0.625rem 1.25rem;
  background: var(--color-text);
  color: var(--color-bg);
  border-radius: var(--radius);
  font-weight: 500;
  font-size: 0.875rem;
  transition: background 150ms ease;
}

.card-btn:hover {
  background: var(--color-accent);
}

Pasul 5: Aranjarea card-urilor în rând

Aici vine limita acestui modul. Pentru o galerie adevărată cu 3 carduri pe rând, avem nevoie de Flexbox sau Grid. Pentru moment, folosim inline-block:

.galerie {
  /* Spațiu între carduri - margin-ul de pe card face treaba */
}

.card {
  display: inline-block;
  vertical-align: top;
  margin-right: 1.5rem;
  margin-bottom: 1.5rem;
}

Pe ecrane mari, cardurile vor curge unul după altul orizontal. Pe ecrane mici, se vor stivui natural. Nu e perfect — spațierile pot fi ciudate — dar funcționează.

Preview: cu Flexbox/Grid va fi mult mai bine

Layout-ul cu inline-block are limitări: spații ciudate între carduri, aliniere dificilă. În Modulul 4 vom folosi display: flex și totul devine:

.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.card {
  flex: 1 1 280px; /* crește, se micșorează, minim 280px */
}

Mai puțin cod, rezultat mai bun. Dar fundamentele rămân aceleași — ce ai învățat în modulele 1-3 se aplică peste tot.

Ce am folosit din fiecare modul

  • Modulul 1 (HTML): article, section, semantică, alt pe imagini
  • Modulul 2 (CSS Basics): variabile, reset, tipografie, fonturi Google
  • Modulul 3 (Box Model): padding/margin, border-radius, box-shadow stratificat, overflow hidden pentru colțuri curate, line-clamp

Provocare: personalizează

Ia codul, schimbă ceva:

  • Schimbă paleta de culori (încearcă o temă albastru-oțel, sau verde-pădure)
  • Ajustează border-radius — testează 6px vs 20px, vezi ce aer dă
  • Fă umbra mai puternică sau mai subtilă
  • Schimbă tipografia (înlocuiește Instrument Serif cu alt font display)
  • Adaugă un tag „Nou" sau „Reducere" cu position (vezi Modulul 5)
Recapitulare
  • Un card profesional = fundal + border-radius + overflow hidden + box-shadow subtilă
  • Hover state cu transform și umbră mai puternică = interactivitate plăcută
  • line-clamp face textul uniform indiferent de conținut
  • Variabile CSS fac schimbarea temei instant
  • Cu Flexbox/Grid (modulele următoare) vei obține același card cu layout mai bun

🎉 Ai terminat Modulul 3!
Stăpânești box model-ul. Modulul 4 (Flexbox) e unde CSS-ul devine cu adevărat puternic pentru layout.

Modulul 4 · Lecția 18 min citire

Ce e Flexbox

Ani de zile, aranjarea lucrurilor pe o pagină era un coșmar. Ca să pui trei carduri unul lângă altul trebuia să lupți cu float, să rezolvi bug-uri de clearfix, să-ți scrii hack-uri ciudate. Apoi a apărut Flexbox și tot peisajul s-a schimbat.

Flexbox e modul modern de a aranja conținut într-o direcție — pe orizontală sau pe verticală. Pentru 90% din layout-urile tale, Flexbox e răspunsul.

Problema pe care o rezolvă

Imaginează-ți: trei carduri, vrei să stea unul lângă altul, echidistant, toate aceeași înălțime chiar dacă conținutul diferă. Înainte de Flexbox, asta cerea 50 de linii de CSS. Cu Flexbox, 3.

.container {
  display: flex;
  gap: 1rem;
}

Atât. Container-ul devine „flex", copiii lui se aranjează orizontal cu spațiu între ei. Magia începe aici.

Container vs items — terminologia esențială

Flexbox vorbește despre două lucruri:

  • Flex container — elementul pe care ai scris display: flex
  • Flex items — copiii direcți ai container-ului (și doar ei — nepoții nu sunt flex items)

Regulile flex se aplică pe container (cum se aranjează items-urile) SAU pe items (cum se comportă fiecare individual). O să le vedem pe toate în următoarele lecții.

Axe: principală și transversală

Cheia înțelegerii Flexbox e conceptul de axe:

  • Axa principală (main axis) — direcția în care se aranjează items-urile. Implicit: orizontală (stânga-dreapta).
  • Axa transversală (cross axis) — perpendiculară pe cea principală. Implicit: verticală (sus-jos).

Când îți setezi flex-direction: column, axele se inversează — principala devine verticală, transversala devine orizontală.

De ce contează axele

Două dintre cele mai importante proprietăți Flexbox — justify-content și align-items — lucrează pe axe diferite:

  • justify-content lucrează pe axa principală
  • align-items lucrează pe axa transversală

Dacă inversezi direcția (column vs row), inversezi și efectele acestor proprietăți. Dev-erii noi se împiedică aici des. Ține minte: justify = principal, align = transversal.

Primul tău layout Flexbox

<nav class="meniu">
  <a href="/">Acasă</a>
  <a href="/despre">Despre</a>
  <a href="/contact">Contact</a>
</nav>
.meniu {
  display: flex;
  gap: 2rem;
}

Fără Flexbox, link-urile ar fi stat unul sub altul (dacă ar fi fost block) sau fără spațiere (dacă ar fi fost inline). Cu cele două linii de mai sus — navbar orizontal perfect spațiat.

Al doilea exemplu — coloană

.formular {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

Acum copiii se aranjează pe verticală, cu 1rem între ei. Formularele arată consistent fără să te mai gândești la margini.

Când folosești Flexbox

Flexbox e pentru aranjare într-o singură direcție — rând SAU coloană. Dacă ai nevoie de layout 2D (rânduri ȘI coloane cu structură), folosești Grid (Modulul 5).

Flexbox e perfect pentru:

  • Navbar-uri
  • Butoane cu iconițe
  • Formulare verticale
  • Carduri unul lângă altul
  • Layout-uri tip „titlu + subtitlu + buton" aliniate
  • Centrare (orice)
Exemplu explicat
Centrare perfectă cu Flexbox
.container {
  display: flex;
  justify-content: center; /* centrat orizontal */
  align-items: center;     /* centrat vertical */
  min-height: 100vh;       /* ocupă tot ecranul */
}

.card {
  padding: 2rem;
  background: white;
}
Problema „centrează o cutie pe mijlocul paginii" a chinuit dev-erii 20 de ani. Cu Flexbox, 3 linii. Asta e diferența.
Prima verificare
Care e terminologia corectă — unde aplici display: flex?
Recapitulare
  • Flexbox = aranjarea conținutului într-o direcție (rând sau coloană)
  • display: flex merge pe container, nu pe copii
  • Axa principală = direcția de aranjare; axa transversală = perpendiculară
  • justify-content lucrează pe axa principală
  • align-items lucrează pe axa transversală
  • Pentru layout 2D (ex. dashboard) folosești Grid (Modulul 5)
Modulul 4 · Lecția 27 min citire

flex-direction și flex-wrap

Primul lucru pe care-l decizi despre un layout Flexbox: în ce direcție se aranjează lucrurile? Al doilea: ce se întâmplă dacă nu mai încap?

flex-direction — stabilește axa principală

.container {
  display: flex;
  flex-direction: row;          /* implicit — orizontal, stg la dr */
  /* flex-direction: row-reverse;  orizontal, dr la stg */
  /* flex-direction: column;       vertical, sus la jos */
  /* flex-direction: column-reverse; vertical, jos la sus */
}
  • row — copiii curg orizontal, de la stânga la dreapta
  • column — copiii curg vertical, de sus în jos
  • Variantele -reverse inversează ordinea

Când folosești column

Majoritatea layout-urilor mari (pagini întregi) sunt column — titluri, paragrafe, secțiuni, footer. Curg vertical.

main {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

Asta spațiază automat toate secțiunile unui main cu 2rem între ele. Fără margins, fără colapsări.

Când folosești row

Pentru componente mici aranjate orizontal:

.card-footer {
  display: flex;
  /* row e implicit, nu trebuie specificat */
  gap: 1rem;
  align-items: center;
}

Un footer de card cu preț + buton, aliniate pe aceeași linie.

flex-wrap — ce se întâmplă când conținutul e prea mare

Implicit, flex items nu se înfășoară. Dacă pui 6 carduri într-un rând și lățimea lor totală depășește container-ul, toate se îngustează să încapă. Pot ajunge ilizibil de înguste.

.galerie {
  display: flex;
  flex-wrap: wrap; /* items-urile care nu încap sar pe rândul următor */
  gap: 1rem;
}

Valorile flex-wrap:

  • nowrap (implicit) — toate pe un rând, chiar dacă se înghesuie
  • wrap — sar pe rândul următor când nu mai încap
  • wrap-reverse — wrap, dar în ordinea inversă (rar folosit)

Prescurtarea flex-flow

Pentru direction + wrap într-o singură declarație:

.galerie {
  display: flex;
  flex-flow: row wrap;
  /* echivalent cu:
     flex-direction: row;
     flex-wrap: wrap; */
  gap: 1rem;
}

Nu e obligatoriu — multe proiecte scriu cele două proprietăți separate pentru claritate. Folosește ce ți se pare mai lizibil.

Exemplu explicat
Galerie de carduri responsive simplă
.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.card {
  flex: 1 1 280px; /* despre asta în lecția 4-5 */
  background: white;
  border-radius: 10px;
  padding: 1.5rem;
}
Combinația flex-wrap: wrap + lățime minimă pe card = galerie automat responsive. Pe ecrane mari, 4-5 carduri pe rând. Pe ecrane mici, se pliază. Fără media queries.
Când să nu folosești wrap

Există layout-uri unde wrap-ul distruge design-ul: navbar-uri simple, butoane grupate, liste orizontale scurte. Dacă items-urile tale trebuie mereu să stea pe o singură linie, nu seta flex-wrap: wrap.

Pentru conținut care chiar trebuie să se încadreze indiferent de spațiu, consideră overflow-x: auto pe container — devine scroll orizontal (util pentru tab-uri multe, carousel-uri simple).

Decide ce setezi
Faci un layout de pagină cu header sus, content în mijloc, footer jos. Toate ocupă lățimea completă. Cum configurezi body-ul?
Recapitulare
  • flex-direction: row (implicit) pentru orizontal, column pentru vertical
  • flex-wrap: wrap permite items-urilor să sară pe rânduri noi când nu mai încap
  • flex-wrap: nowrap (implicit) le forțează pe o singură linie
  • flex-flow: row wrap = prescurtare pentru ambele
  • Pentru page layout: column. Pentru elemente grupate: row.
Modulul 4 · Lecția 38 min citire

justify-content

Ai items aranjate într-o direcție. Acum, cum le poziționezi în interiorul container-ului? Toate la stânga? Toate la dreapta? Egal distanțate? justify-content face exact asta — aliniează items-urile pe axa principală.

Cele șase valori

Imaginează-ți un container cu 3 carduri și spațiu în plus. Cum distribuim acel spațiu?

flex-start (implicit)

Items-urile se adună la începutul container-ului. Spațiul liber e la final.

.container {
  display: flex;
  justify-content: flex-start;
  /* [item1][item2][item3]___spațiu___ */
}

flex-end

Items-urile se adună la finalul container-ului.

.container {
  display: flex;
  justify-content: flex-end;
  /* ___spațiu___[item1][item2][item3] */
}

Util pentru butoane „Anulează / Salvează" aliniate la dreapta într-un modal.

center

Items-urile se centrează. Spațiul se distribuie egal la început și la sfârșit.

.container {
  display: flex;
  justify-content: center;
  /* __[item1][item2][item3]__ */
}

space-between

Primul item la început, ultimul la sfârșit, spațiul distribuit între ele.

.container {
  display: flex;
  justify-content: space-between;
  /* [item1]___[item2]___[item3] */
}

Pattern clasic pentru navbar: logo la stânga, meniu la dreapta.

space-around

Spațiul e distribuit în jurul fiecărui item. Efectul: spațiile dintre items sunt duble față de spațiile de la margini.

.container {
  display: flex;
  justify-content: space-around;
  /* _[item1]__[item2]__[item3]_ */
}

space-evenly

Spațiul egal peste tot — la margini, între items, exact același.

.container {
  display: flex;
  justify-content: space-evenly;
  /* __[item1]__[item2]__[item3]__ */
}
space-between vs space-around vs space-evenly

Confuză la început. Regula simplă:

  • space-between — zero spațiu la margini, maxim spațiu între
  • space-evenly — spațiu egal peste tot (cel mai „natural" pentru majoritatea designurilor)
  • space-around — spațiu în jurul fiecărui element (duble între items, simple la margini)

Pentru majoritatea cazurilor: space-between pentru navbar-uri, space-evenly pentru distribuție uniformă. space-around e rar util.

Pattern clasic: navbar cu logo și link-uri

<header>
  <div class="logo">Școala</div>
  <nav>
    <a href="/">Acasă</a>
    <a href="/despre">Despre</a>
  </nav>
</header>
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
}

Logo la stânga, nav la dreapta — space-between împinge automat. Dacă ai 3 grupuri (logo, meniu, profil user), space-between le distribuie pe toate trei corect.

Pattern: buton izolat la final

Ce faci dacă ai items multe și vrei ultimul la dreapta?

.toolbar {
  display: flex;
  gap: 0.5rem;
}

.toolbar .logout {
  margin-left: auto; /* magie! */
}

margin-left: auto pe un item flex îl împinge la dreapta de tot. Funcționează pentru că margin auto absoarbe tot spațiul disponibil. Truc ultra-folosit.

Exemplu explicat
Butoane modal aliniate
<div class="modal-actions">
  <button class="secondary">Anulează</button>
  <button class="primary">Salvează</button>
</div>
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
}
Butoanele sunt împinse la dreapta — convenție UI standard pentru modale. Spațiul dintre ele e 0.75rem, controlat de gap.
Alege setarea potrivită
Ai 4 iconițe de social media într-un rând. Vrei spațiu egal între ele și la margini — distribuție perfect uniformă. Ce valoare folosești?
Recapitulare
  • justify-content aliniază items pe axa principală (unde curg)
  • flex-start, flex-end, center — grupare la început/sfârșit/mijloc
  • space-between — primul/ultimul la margini, spațiu între
  • space-evenly — spațiu egal peste tot (cel mai natural)
  • space-around — spațiu în jurul fiecărui item
  • Truc: margin-left: auto împinge un item la dreapta
Modulul 4 · Lecția 48 min citire

align-items

justify-content aliniează pe axa principală. align-items face același lucru — dar pe axa transversală. Dacă flex-direction e row, align-items controlează alinierea verticală.

De ce e atât de util

Să zicem că ai un card cu titlu și buton unul lângă altul. Titlul are 2 linii, butonul are una. Cum le aliniezi? Centrate? Titlul sus și butonul sus? Ambele jos? align-items rezolvă asta instant.

Valorile

stretch (implicit)

Items-urile se întind să umple container-ul pe axa transversală. Dacă flex-direction e row, toate items-urile vor avea aceeași înălțime — egală cu cel mai înalt.

.galerie {
  display: flex;
  align-items: stretch; /* implicit — nu trebuie scris */
}

/* Toate cardurile vor avea aceeași înălțime */

Util pentru galerii de carduri unde vrei înălțime uniformă chiar dacă unele au mai mult conținut.

flex-start

Items-urile se aliniază la începutul axei transversale (sus, dacă flex-direction e row).

flex-end

La sfârșitul axei transversale (jos pentru row).

center

Centrate pe axa transversală.

.card-footer {
  display: flex;
  align-items: center; /* preț și buton aliniate la mijloc vertical */
  justify-content: space-between;
}

baseline

Items-urile se aliniază după baseline-ul textului. Util când ai texte de dimensiuni diferite pe aceeași linie și vrei ca textul să fie aliniat frumos indiferent de dimensiunea elementelor.

.pret-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.pret-mare { font-size: 3rem; }
.valuta { font-size: 1rem; }

Aici „€" și cifra mare de preț au baseline-ul aliniat, chiar dacă font-size-urile diferă mult.

Centrare verticală reală — problema legendară rezolvată

„Cum centrez vertical ceva?" — una dintre cele mai căutate întrebări pe Stack Overflow. Răspunsul modern:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

Trei linii, problema rezolvată. Orice ai în container, va fi perfect centrat pe ambele axe. Before Flexbox, asta cerea 20+ linii cu hack-uri.

align-self — suprascrie pentru un singur item

align-items merge pe container și afectează toți copiii. Dacă vrei ca un singur item să se alinieze diferit, pui align-self pe acel item:

.toolbar {
  display: flex;
  align-items: center;
}

.toolbar .logo {
  align-self: flex-start; /* logo-ul se aliniează sus, restul rămâne centrat */
}
Exemplu explicat
Card cu footer aliniat jos
.card {
  display: flex;
  flex-direction: column;
  padding: 1.5rem;
  min-height: 300px;
}

.card-title {
  font-size: 1.5rem;
}

.card-desc {
  flex-grow: 1; /* crește să ocupe spațiul liber */
}

.card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 1rem;
  border-top: 1px solid #eee;
}
Pattern important: cardul e flex-column cu min-height. Descrierea cu flex-grow: 1 împinge footer-ul în jos. Footer-ul e un sub-flex cu preț la stânga și buton la dreapta, ambele aliniate pe baseline cu align-items: center.

align-content — pentru flex-wrap

Când ai flex-wrap: wrap și items-urile se întind pe mai multe rânduri, align-content controlează cum sunt distribuite rândurile pe axa transversală.

.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  min-height: 500px;
  align-content: flex-start; /* rândurile se adună sus */
  /* align-content: center;       toate rândurile centrate vertical */
  /* align-content: space-between; primul sus, ultimul jos */
}

Rar folosit, dar util când ai un container mai înalt decât conținutul și vrei control peste distribuția rândurilor.

Problema clasică
Vrei să centrezi un card perfect pe mijlocul ecranului (orizontal ȘI vertical). Ce setări aplici pe container?
Recapitulare
  • align-items aliniează pe axa transversală (perpendiculară pe direcția principală)
  • stretch (implicit) — items-urile umplu axa transversală (aceeași înălțime pentru row)
  • center, flex-start, flex-end — grupare pe axa transversală
  • baseline — aliniază după linia de bază a textului
  • align-self suprascrie alinierea pentru un singur item
  • Centrare perfectă: justify-content: center + align-items: center + înălțime
Modulul 4 · Lecția 510 min citire

flex: grow, shrink, basis

Până acum am controlat containerul. Acum învățăm cum să controlezi items-urile individual — cât cresc, cât se micșorează, cât de lat vor fi ideal. Astea sunt cele trei proprietăți care fac layout-urile flex cu adevărat puternice.

Proprietățile pe items

Acestea se aplică pe items, nu pe container:

  • flex-grow — cât crește un item dacă e spațiu în plus
  • flex-shrink — cât se micșorează dacă nu e destul spațiu
  • flex-basis — lățimea ideală înainte de creștere/micșorare

flex-grow — împarte spațiul liber

Dacă ai 3 items-uri și spațiu liber în container, cine primește spațiul?

.item {
  flex-grow: 0; /* implicit — nu crește */
}

.item-special {
  flex-grow: 1; /* primește tot spațiul liber */
}

Cu proporții:

.item-1 { flex-grow: 1; } /* primește 1 parte */
.item-2 { flex-grow: 2; } /* primește 2 părți (dublu) */
.item-3 { flex-grow: 1; } /* primește 1 parte */

Spațiul liber se distribuie proporțional: 1/4, 2/4, 1/4.

flex-shrink — cine cedează când nu e loc

Default: toate items-urile au flex-shrink: 1 — se micșorează egal dacă nu încap.

.logo {
  flex-shrink: 0; /* niciodată nu se micșorează */
}

.titlu {
  flex-shrink: 1; /* se micșorează normal */
}

Setând flex-shrink: 0 pe un element, îi garantezi lățimea. Folosit mult pentru logo-uri, iconițe fixe — care trebuie să-și păstreze dimensiunea.

flex-basis — dimensiunea de pornire

flex-basis stabilește lățimea ideală a item-ului înainte să aplice grow sau shrink.

.card {
  flex-basis: 300px; /* pornește de la 300px, apoi crește/se micșorează */
}

Similar cu width, dar cu o diferență: flex-basis e „punctul de pornire", iar flex-grow/flex-shrink ajustează de acolo.

Prescurtarea flex

În loc să scrii cele 3 proprietăți separate, folosește prescurtarea:

.card {
  flex: [grow] [shrink] [basis];
}

/* Exemplu concret */
.card {
  flex: 1 1 280px;
  /* = flex-grow: 1; flex-shrink: 1; flex-basis: 280px; */
}

Cele mai comune formule flex

flex: 1

Cea mai folosită. Echivalent cu flex: 1 1 0 — crește, se micșorează, pornește de la 0.

.sidebar {
  flex: 0 0 240px; /* 240px fix, nu crește, nu se micșorează */
}

.content {
  flex: 1; /* ocupă tot restul */
}

Pattern de layout ultra-folosit: sidebar fix + content care umple restul.

flex: 1 1 280px — carduri responsive

Folosit pentru galerii de carduri care se adaptează automat la ecran:

.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.card {
  flex: 1 1 280px;
}

Fiecare card pornește la 280px (basis), dar crește să umple rândul (grow: 1) și se poate micșora dacă e nevoie. Plus flex-wrap: wrap le lasă să sară pe rânduri noi. Rezultat: galerie care se reorganizează singură când schimbi lățimea ecranului.

flex: 0 0 auto — dimensiune rigidă

Folosit pentru elemente care trebuie să-și păstreze exact dimensiunea naturală:

.icon {
  flex: 0 0 auto; /* exact atât cât e conținutul, nu se schimbă */
}
flex: 1 vs width: 100%

Par similare, dar diferă:

  • width: 100% — „100% din părinte", ignoră alte items. Poate face elementul să iasă afară.
  • flex: 1 — „ia tot spațiul rămas după ce celelalte items s-au aranjat". Respectă alte items și nu iese afară.

În context Flexbox, folosește mereu flex: 1 pentru „umple restul", nu width: 100%.

Exemplu explicat
Layout clasic aplicație: sidebar + main + panel
.app {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  flex: 0 0 240px; /* 240px fix */
  background: #f4f2ec;
}

.main {
  flex: 1; /* tot spațiul rămas */
  padding: 2rem;
}

.panel {
  flex: 0 0 320px; /* 320px fix (când e vizibil) */
  background: #fbfaf7;
  border-left: 1px solid #eee;
}
Un layout de aplicație tipică (gândește-te la Slack, Notion): sidebar stâng fix, main central fluid, panel drept fix. Flex face asta cu 3 reguli simple — niciun media query, niciun calculc manual.
Formula potrivită
Vrei un sidebar de 200px fix pe stânga și un main care ocupă tot restul spațiului. Ce pui pe fiecare?
Recapitulare
  • flex-grow — cât crește un item când e spațiu liber (0 = nu crește)
  • flex-shrink — cât se micșorează când nu încape (0 = nu se micșorează)
  • flex-basis — lățimea ideală de pornire
  • flex: 1 = umple tot spațiul rămas (cea mai comună)
  • flex: 0 0 200px = dimensiune fixă care nu se schimbă
  • flex: 1 1 280px = card responsive (pentru galerii wrap)
Modulul 4 · Lecția 66 min citire

Gap — spațierea modernă

Timp de ani, spațierea între flex items se făcea cu margini pe copii — și cu tot bagajul de bug-uri și ajustări care veneau cu asta. Apoi a venit gap, cea mai simplă și mai elegantă proprietate din Flexbox modern.

Sintaxa de bază

.container {
  display: flex;
  gap: 1rem;
}

O linie. Spațiul între toate items-urile e 1rem. Între ele, nu la margini. Nu trebuie să te gândești la primul/ultimul item, nu trebuie margini negative pe container, nu trebuie hack-uri.

Gap pe două axe

Când ai flex-wrap: wrap, poți seta spațieri diferite între rânduri și coloane:

.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem 1rem;
  /* 2rem între rânduri, 1rem între coloane */
}

Sau prescurtat:

.galerie {
  gap: 2rem 1rem; /* row-gap column-gap */
}

/* Sau pe proprietăți separate */
.galerie {
  row-gap: 2rem;
  column-gap: 1rem;
}

De ce e mult mai bun decât margini

Înainte de gap, spațierea se făcea așa:

/* Varianta veche — problematică */
.item {
  margin-right: 1rem;
}

.item:last-child {
  margin-right: 0; /* ca să nu ai spațiu în plus la final */
}

Probleme cu varianta veche:

  • Trebuie să știi care e primul sau ultimul (complicat cu flex-wrap)
  • Margine în plus când items-urile se înfășoară pe rând nou
  • Nu merge pe două direcții simultan
  • Multe linii de cod pentru o chestie simplă

Cu gap: o proprietate, totul funcționează.

Gap funcționează și în Grid

gap funcționează identic în CSS Grid (Modulul 5). Ambele folosesc același concept de spațiere între items. Învață odată, folosești peste tot.

Exemplu explicat
Formular vertical cu spațiere consistentă
.formular {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.form-grup {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
<form class="formular">
  <div class="form-grup">
    <label for="email">Email</label>
    <input type="email" id="email">
  </div>

  <div class="form-grup">
    <label for="mesaj">Mesaj</label>
    <textarea id="mesaj"></textarea>
  </div>

  <button type="submit">Trimite</button>
</form>
Formularul (flex column) are 1.5rem între grupuri. Fiecare grup (flex column) are 0.5rem între label și input. Zero margini scrise manual. Curat.
Când NU folosești gap

Rar, dar există cazuri:

  • Când vrei spațieri diferite între anumite items (un item special cu margin-top mai mare)
  • Când ai nevoie de separatori vizibili între items (linii, puncte — deși astea se fac mai bine cu borduri sau pseudo-elemente)

În 95% din cazuri însă, gap e răspunsul corect.

Verificare rapidă
Ai un container flex cu 5 items-uri. Vrei 1rem între fiecare item. Care e cea mai curată soluție?
Recapitulare
  • gap: 1rem pe flex container = spațiu între toate items-urile
  • Spațierea e doar între items, niciodată la margini
  • gap: 2rem 1rem pentru row-gap și column-gap diferite
  • Funcționează identic în CSS Grid
  • Înlocuiește complet margini pe copii pentru spațiere
Modulul 4 · Lecția 710 min citire

Pattern-uri comune

Cunoști regulile Flexbox. Acum hai să le aplici la layout-urile pe care le vei face în fiecare proiect. Șase pattern-uri care acoperă 90% din munca reală.

Pattern 1: Navbar cu logo și meniu

<header>
  <div class="logo">Școala</div>
  <nav>
    <a>Acasă</a>
    <a>Cursuri</a>
    <a>Contact</a>
  </nav>
</header>
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
}

nav {
  display: flex;
  gap: 2rem;
}

Logo la stânga, nav la dreapta, spațiu între link-uri. Două nivele de flex — clasic și puternic.

Pattern 2: Card cu buton împins jos

<article class="card">
  <h3>Titlu</h3>
  <p>Descriere...</p>
  <button>Click</button>
</article>
.card {
  display: flex;
  flex-direction: column;
  min-height: 280px;
}

.card p {
  flex-grow: 1; /* descrierea umple spațiul */
}

Când ai mai multe carduri, butoanele lor vor fi aliniate la aceeași înălțime chiar dacă descrierile diferă. Truc esențial pentru galerii uniforme.

Pattern 3: Sticky footer

„Footer-ul să stea jos de tot, chiar dacă conținutul e scurt":

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1; /* main-ul crește să ocupe restul */
}

footer {
  /* footer-ul rămâne la dimensiunea lui naturală, dar împins jos */
}

Pattern 4: Galerie de carduri responsive

Pattern-ul care a transformat modul cum scriem layout-uri:

.galerie {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.card {
  flex: 1 1 280px;
}

Pe desktop, 4 carduri pe rând. Pe tabletă, 2-3. Pe mobil, 1. Fără media queries, se adaptează singur.

Pattern 5: Meniu cu icon și text

<a class="meniu-item">
  <svg class="icon">...</svg>
  <span>Setări</span>
</a>
.meniu-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem;
}

.icon {
  flex-shrink: 0; /* icon-ul nu se micșorează dacă textul e lung */
  width: 20px;
  height: 20px;
}

align-items: center aliniază iconița și textul pe același centru vertical, indiferent de înălțimea lor.

Pattern 6: Holy Grail — layout clasic cu 3 coloane

Header sus, 3 coloane la mijloc (sidebar + main + panel), footer jos.

.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.middle {
  display: flex;
  flex: 1;
}

.sidebar { flex: 0 0 200px; }
.main { flex: 1; }
.panel { flex: 0 0 240px; }

Pattern-ul ăsta chinuia pe toată lumea înainte de Flexbox. Cu Flexbox, câteva linii.

Flexbox are limite — când să folosești Grid

Flexbox e excelent pentru o direcție. Dacă ai nevoie de layout 2D cu rânduri ȘI coloane care se corelează — de exemplu un tabel de dashboard, sau o pagină complexă cu multe zone — Grid e răspunsul (Modulul 5).

Regula simplă: dacă aranjezi într-un rând sau o coloană = Flexbox. Dacă aranjezi într-o matrice 2D = Grid.

Exemplu explicat
Hero section complet
<section class="hero">
  <div class="hero-content">
    <h1>Titlu mare</h1>
    <p>Subtitlu explicativ</p>
    <div class="cta-group">
      <button class="primary">Începe</button>
      <button class="secondary">Află mai multe</button>
    </div>
  </div>
</section>
.hero {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 80vh;
  padding: 2rem;
  text-align: center;
}

.hero-content {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  max-width: 640px;
}

.cta-group {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap; /* pe mobil sar pe două rânduri */
}
Trei nivele de flex: hero-ul centrează conținutul; conținutul e coloană cu spațiu; butoanele sunt rând. Fiecare nivel face doar ce e responsabil să facă.
Pattern recognition
Ai 3 carduri într-un rând și unele au mai mult conținut. Vrei ca toate să aibă aceeași înălțime. Ce setare de Flexbox face asta automat?
Recapitulare
  • Navbar: justify-content: space-between + align-items: center
  • Card uniform cu footer jos: flex-direction: column + flex-grow: 1 pe textul central
  • Sticky footer: body flex-column + flex: 1 pe main
  • Galerie responsive: flex-wrap: wrap + flex: 1 1 280px
  • Icon + text aliniate: align-items: center + gap
  • Pentru layout 2D complex (nu doar un rând sau o coloană), folosește Grid (Modulul 5)
Modulul 4 · Lecția 815 min citire

Proiect: navbar și features

Ai Flexbox. Hai să construim ceva care să fie efectiv util: un navbar profesional urmat de o secțiune de features cu carduri. Două componente pe care le vei refolosi în orice proiect.

Ce construim

Primul: un navbar modern cu logo la stânga, link-uri în centru și un buton CTA la dreapta. Al doilea: o secțiune de features cu 3 carduri aliniate, care se reorganizează natural pe mobil.

Pasul 1: HTML-ul complet

<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;700&family=Instrument+Serif&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="styles.css">
  <title>Școala de Web</title>
</head>
<body>

  <!-- NAVBAR -->
  <header class="navbar">
    <a class="logo" href="/">
      <div class="logo-mark">S</div>
      <span>Școala</span>
    </a>
    <nav class="nav-links">
      <a href="#">Cursuri</a>
      <a href="#">Blog</a>
      <a href="#">Despre</a>
    </nav>
    <a class="cta" href="#">Începe</a>
  </header>

  <!-- FEATURES -->
  <section class="features">
    <h2 class="features-title">De ce <em>noi</em></h2>
    <p class="features-lead">Trei motive pentru care dev-erii aleg Școala</p>

    <div class="features-grid">
      <article class="feature-card">
        <div class="feature-icon">✦</div>
        <h3>Proiecte reale</h3>
        <p>Nu doar teorie. Construiești ce folosești și în muncă.</p>
        <a href="#" class="feature-link">Vezi cursuri →</a>
      </article>

      <article class="feature-card">
        <div class="feature-icon">◆</div>
        <h3>Exerciții interactive</h3>
        <p>Scrii cod în browser, primești feedback instant. Fără setup.</p>
        <a href="#" class="feature-link">Vezi demo →</a>
      </article>

      <article class="feature-card">
        <div class="feature-icon">●</div>
        <h3>Mentorat direct</h3>
        <p>Te blochezi, întrebi, primești răspuns. Nu ești singur.</p>
        <a href="#" class="feature-link">Întreabă-ne →</a>
      </article>
    </div>
  </section>

</body>
</html>

Pasul 2: Reset și design tokens

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
body { line-height: 1.5; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: none; }

/* Tokens */
:root {
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-bg: #fbfaf7;
  --color-panel: #ffffff;
  --color-accent: #c8553d;
  --color-border: rgba(0, 0, 0, 0.08);

  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;

  --radius: 10px;
  --radius-lg: 14px;
}

body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
}

Pasul 3: Navbar-ul

.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-panel);
}

.logo {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-family: var(--font-display);
  font-size: 1.5rem;
}

.logo-mark {
  width: 32px;
  height: 32px;
  background: var(--color-text);
  color: var(--color-bg);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-style: italic;
  flex-shrink: 0;
}

.nav-links {
  display: flex;
  gap: 2rem;
}

.nav-links a {
  color: var(--color-text-soft);
  font-weight: 500;
  transition: color 150ms ease;
}

.nav-links a:hover {
  color: var(--color-accent);
}

.cta {
  padding: 0.625rem 1.25rem;
  background: var(--color-text);
  color: var(--color-bg);
  border-radius: var(--radius);
  font-weight: 500;
  transition: background 150ms ease;
}

.cta:hover {
  background: var(--color-accent);
}

Observă: navbar-ul folosește space-between — logo la stânga, link-uri la mijloc (grup central), buton la dreapta. Flexbox împinge grupurile perfect.

Pasul 4: Secțiunea Features

.features {
  padding: 5rem 2rem;
  max-width: 1200px;
  margin: 0 auto;
  text-align: center;
}

.features-title {
  font-family: var(--font-display);
  font-size: 3rem;
  font-weight: 400;
  letter-spacing: -0.02em;
  margin-bottom: 0.5rem;
}

.features-title em {
  font-style: italic;
  color: var(--color-accent);
}

.features-lead {
  color: var(--color-text-soft);
  font-size: 1.125rem;
  margin-bottom: 3rem;
}

.features-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  justify-content: center;
}

.feature-card {
  flex: 1 1 280px; /* responsive automat! */
  max-width: 380px;
  padding: 2rem;
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
  text-align: left;

  display: flex;
  flex-direction: column;
  gap: 0.75rem;

  transition: transform 200ms ease, box-shadow 200ms ease;
}

.feature-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
}

.feature-icon {
  width: 44px;
  height: 44px;
  background: var(--color-accent);
  color: white;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  margin-bottom: 0.5rem;
}

.feature-card h3 {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 400;
}

.feature-card p {
  color: var(--color-text-soft);
  flex-grow: 1; /* împinge link-ul jos */
}

.feature-link {
  color: var(--color-accent);
  font-weight: 500;
  margin-top: 0.5rem;
  transition: transform 150ms ease;
  display: inline-block;
}

.feature-link:hover {
  transform: translateX(3px);
}

Ce face această soluție specială

Layout-ul e complet responsive fără niciun media query. Iată cum:

  • flex-wrap: wrap pe grid permite cardurilor să sară pe rânduri noi
  • flex: 1 1 280px pe fiecare card le dă o lățime minimă (280px) dar le lasă să crească
  • Pe ecrane mari: 3 carduri pe rând
  • Pe tabletă: 2 carduri pe rând, unul singur jos
  • Pe mobil: 1 card pe rând
  • Totul fără să scrii o singură media query

Cardurile sunt perfect uniforme. Chiar dacă descrierile diferă în lungime:

  • align-items: stretch (implicit) le face aceeași înălțime
  • flex-direction: column pe card le organizează vertical
  • flex-grow: 1 pe paragraf împinge link-ul la fund, uniform pe toate cardurile
Navbar-ul trebuie făcut responsive

Versiunea de mai sus arată bine pe desktop și tabletă, dar pe ecrane sub 640px, link-urile ar trebui să devină un meniu hamburger. Asta implică JavaScript (deschide/închide meniul) și media queries — subiecte pentru modulele viitoare.

Pentru moment, pe mobil probabil vei avea scroll orizontal sau link-urile se vor suprapune. Îl vom repara în Modulul 6 (Responsive Design).

Recapitulare
  • Navbar pattern: justify-content: space-between + align-items: center
  • Features grid: flex-wrap: wrap + flex: 1 1 280px = responsive gratuit
  • Carduri uniforme ca înălțime: align-items: stretch (implicit Flexbox)
  • Link în subsol aliniat: flex-direction: column + flex-grow: 1 pe paragraf
  • Hover cu transform + shadow = feedback vizual plăcut

🎉 Ai terminat Modulul 4!
Stăpânești Flexbox — cea mai folosită unealtă de layout din CSS. Modulul 5 (Grid) e pentru layout-uri 2D complexe.

Modulul 5 · Lecția 17 min citire

Ce e Grid

Flexbox aranjează într-o direcție. Grid aranjează în două. Dacă Flexbox e un raft cu cărți, Grid e o bibliotecă cu rafturi pe rânduri și coloane. Ambele utile — dar pentru lucruri diferite.

Diferența fundamentală

Flexbox e uni-direcțional: aranjezi lucrurile ori în rând, ori în coloană. Items-urile curg în flow natural, se înfășoară dacă e nevoie.

Grid e bi-direcțional: definești rânduri ȘI coloane simultan, iar items-urile se plasează într-un sistem de celule — ca un spreadsheet.

Când folosești fiecare

Flexbox pentru:

  • Navbar-uri
  • Liste de butoane
  • Carduri uniforme într-un rând
  • Formulare verticale
  • Componente mici cu aranjare liniară

Grid pentru:

  • Layout-uri de pagină complete (header, sidebar, main, footer)
  • Dashboard-uri cu zone multiple
  • Galerii masonry
  • Orice layout 2D unde elementele se corelează pe ambele axe
Grid și Flexbox se completează

Nu alege unul „împotriva" celuilalt. Proiectele reale folosesc ambele:

  • Grid pentru layout-ul principal al paginii
  • Flexbox pentru componentele din interiorul zonelor grid

De exemplu: Grid pentru dashboard (sidebar + header + main + widget area), Flexbox pentru fiecare widget în parte.

Primul tău Grid

.container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
}

Trei coloane egale. Orice copil al container-ului intră într-o celulă, de la stânga la dreapta, pe rânduri. Când umpli 3 celule, se creează automat rândul următor.

Exemplu simplu vs echivalent în Flexbox

Vrei 4 carduri pe un rând. În Grid:

.galerie {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}

În Flexbox ar fi:

.galerie {
  display: flex;
  gap: 1rem;
}

.galerie > * {
  flex: 1;
}

Similar ca rezultat pentru 4 carduri pe un rând. Dar Grid-ul iese în față când ai nevoie de rânduri care se corelează — de exemplu, 4 coloane pe 3 rânduri unde cardurile din rândul 2 sunt aliniate cu cele din rândul 1.

Terminologia Grid

Câteva cuvinte pe care le vei întâlni peste tot:

  • Grid container — elementul pe care ai display: grid
  • Grid items — copiii direcți ai container-ului
  • Grid lines — liniile imaginare dintre coloane și rânduri (numerotate 1, 2, 3...)
  • Grid tracks — coloanele sau rândurile (spațiul între linii)
  • Grid cells — o singură celulă din grid (intersecția unei coloane cu un rând)
  • Grid areas — zone compuse din mai multe celule
Exemplu explicat
Layout de pagină cu Grid
.page {
  display: grid;
  grid-template-columns: 240px 1fr 300px;
  grid-template-rows: auto 1fr auto;
  gap: 1rem;
  min-height: 100vh;
}

.header  { grid-column: 1 / 4; }   /* header-ul se întinde pe toate coloanele */
.sidebar { grid-column: 1; }        /* sidebar stânga */
.main    { grid-column: 2; }        /* conținut principal */
.panel   { grid-column: 3; }        /* panel dreapta */
.footer  { grid-column: 1 / 4; }    /* footer peste tot */
În Flexbox, același layout ar cere nested flex containers și multă gândire. În Grid, definești dimensiunile o dată și plasezi items-urile în zone. Mult mai natural pentru layout-uri complexe.
Alege unealta potrivită
Faci o pagină cu header sus, sidebar stânga, main în centru, panel dreapta și footer jos. Toate trebuie să se alinieze perfect. Ce folosești?
Recapitulare
  • Flexbox = 1 direcție, Grid = 2 direcții (rânduri + coloane)
  • Grid pentru layout-uri de pagină complete, dashboard-uri, orice 2D
  • Flexbox pentru componente simple într-o direcție
  • Se folosesc împreună: Grid pentru layout mare, Flexbox pentru componente înăuntru
  • Terminologia: container, items, lines, tracks, cells, areas
Modulul 5 · Lecția 28 min citire

Coloane și rânduri

Grid-ul nu începe cu nimic — îi spui câte coloane, câte rânduri și ce dimensiune au. Două proprietăți simple rezolvă asta: grid-template-columns și grid-template-rows.

Definirea coloanelor

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  /* trei coloane: 200px, flexibilă, 200px */
}

Fiecare valoare separată prin spațiu e o coloană. Numărul de valori determină câte coloane sunt.

Unități pe care le poți folosi

  • px — dimensiune fixă (200px)
  • % — procent din container (50%)
  • fr — „fraction" din spațiul rămas (o să vedem în lecția următoare)
  • auto — exact cât e conținutul
  • rem / em — relativ la font
/* Amestec de unități */
.layout {
  display: grid;
  grid-template-columns: 250px 1fr auto;
  /* sidebar fix 250px, main care umple, panel ce e cât conținutul */
}

Definirea rândurilor

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 100px 200px 100px;
  /* 2 coloane egale, 3 rânduri cu dimensiuni diferite */
}

De multe ori nu definești rândurile

Grid creează rânduri automat pe măsură ce items-urile umplu spațiul. Nu trebuie să specifici rânduri dacă:

  • Items-urile au înălțime naturală (conținut variabil)
  • Vrei ca Grid să calculeze automat câte rânduri sunt necesare
.galerie {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
  /* fără grid-template-rows — se creează automat */
}

Acesta e cel mai comun pattern. Definești coloanele, rândurile se creează de la sine.

Rânduri implicite cu grid-auto-rows

Dacă vrei să controlezi înălțimea rândurilor create automat:

.galerie {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 200px; /* orice rând creat automat are 200px */
  gap: 1rem;
}
Height pe grid items vs grid-auto-rows

Două moduri diferite de a seta înălțimi:

  • height: 200px pe item — forțează items-ul să fie 200px, rândul se adaptează
  • grid-auto-rows: 200px pe container — forțează rândurile să fie 200px, items-urile se adaptează

Pentru galerii uniforme, grid-auto-rows e mai natural. Fiecare rând e exact cât vrei tu, items-urile se aliniază pe aceeași înălțime automat.

Gap — funcționează identic ca în Flexbox

.galerie {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
  /* sau separat: */
  row-gap: 2rem;
  column-gap: 1rem;
}

Spațiul între celule. Nu la margini, doar între. La fel ca în Flexbox.

Exemplu explicat
Galerie simplă de 3 coloane
.galerie {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1.5rem;
  padding: 1.5rem;
}

.card {
  background: white;
  padding: 1.5rem;
  border-radius: 12px;
}
<div class="galerie">
  <div class="card">Card 1</div>
  <div class="card">Card 2</div>
  <div class="card">Card 3</div>
  <div class="card">Card 4</div>
  <div class="card">Card 5</div>
</div>
5 carduri, 3 coloane: primele 3 pe rândul 1, următoarele 2 pe rândul 2. Grid umple automat rândurile necesare. Spațierea e consistentă în ambele direcții.
Alege sintaxa corectă
Vrei un grid cu 4 coloane egale. Care e cel mai bun mod de a-l defini?
Recapitulare
  • grid-template-columns definește câte coloane și ce dimensiune au
  • grid-template-rows definește rânduri (opțional — se creează automat)
  • Valorile separate prin spațiu = coloane/rânduri separate
  • grid-auto-rows controlează înălțimea rândurilor create automat
  • gap spațiază celule, la fel ca în Flexbox
  • fr e unitatea magică pentru proporții (lecția următoare)
Modulul 5 · Lecția 39 min citire

fr, repeat() și minmax()

Trei unelte care fac Grid-ul cu adevărat flexibil: fr pentru proporții, repeat() pentru scriere scurtă, minmax() pentru limite. Împreună, rezolvă 80% din nevoile tale de layout.

Unitatea fr — fracțiuni din spațiu

fr = „fraction" — o parte din spațiul disponibil rămas după ce dimensiunile fixe au fost scăzute.

.layout {
  display: grid;
  grid-template-columns: 200px 1fr;
  /* 200px fix, apoi 1fr ia tot restul */
}

Cu proporții:

.layout {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  /* coloana din mijloc e dublă față de cele laterale */
}

Total e 4fr (1+2+1). Prima ia 1/4, mijlocul ia 2/4, ultima ia 1/4 din spațiul disponibil.

De ce fr e mai bun decât procente

Cu procente, trebuie să socotești gap-urile manual:

/* Problematic — depășește containerul dacă ai gap */
.galerie {
  display: grid;
  grid-template-columns: 33.33% 33.33% 33.33%;
  gap: 1rem; /* acum depășește! */
}

/* Perfect — fr ia în calcul gap-ul */
.galerie {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
}

fr calculează spațiul rămas după gap și-l împarte. Procentele nu țin cont de gap.

repeat() — scriere scurtă

În loc să scrii 1fr 1fr 1fr 1fr 1fr 1fr, folosești repeat():

.galerie {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  /* echivalent cu: 1fr 1fr 1fr 1fr 1fr 1fr */
}

repeat() cu pattern-uri

repeat() poate repeta și grupuri de valori:

.layout {
  display: grid;
  grid-template-columns: repeat(3, 200px 1fr);
  /* = 200px 1fr 200px 1fr 200px 1fr (6 coloane) */
}

minmax() — limite pentru coloane

minmax(min, max) definește o dimensiune cu limite — cel puțin min, dar maxim max.

.layout {
  display: grid;
  grid-template-columns: minmax(200px, 1fr) 3fr;
  /* prima coloană: minim 200px, maxim 1fr */
  /* a doua coloană: 3fr */
}

Când container-ul e mare, coloana crește la 1fr. Când e mic, se oprește la minimum 200px.

minmax() pentru cards responsive

Cel mai util pattern:

.galerie {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
}

minmax(0, 1fr) rezolvă o problemă subtilă: items-urile nu depășesc niciodată container-ul. Fără min: 0, conținut foarte lung (URL-uri, cod) poate face coloanele să crească peste spațiul disponibil.

minmax(0, 1fr) - de ce contează

Implicit, 1fr înseamnă „o parte din spațiu, dar cel puțin cât conținutul meu". Dacă ai un cuvânt foarte lung (ex. URL), coloana crește să-l încapă — rupând layout-ul.

minmax(0, 1fr) îi spune explicit: „minim 0, poți fi atât cât îți spun". Conținutul lung e trunchiat sau se înfășoară, nu rupe layout-ul.

Folosește minmax(0, 1fr) în loc de 1fr când ai conținut dinamic (texte lungi, cod, link-uri).

Combinarea tuturor

.layout {
  display: grid;
  grid-template-columns: repeat(3, minmax(200px, 1fr));
  gap: 1.5rem;
}

„3 coloane egale, minim 200px fiecare, altfel împart spațiul egal". Concentrat și puternic.

Exemplu explicat
Layout flexibil cu limite
.dashboard {
  display: grid;
  grid-template-columns:
    minmax(200px, 280px)  /* sidebar: între 200 și 280px */
    minmax(0, 1fr)        /* main: umple restul */
    minmax(240px, 320px); /* panel: între 240 și 320px */
  gap: 1rem;
  min-height: 100vh;
}
Sidebar-ul și panel-ul au limite clare — nu devin prea mici (minim) nici prea mari (maxim). Main-ul umple tot ce rămâne, dar nu depășește (minmax 0, 1fr).
Combinație optimă
Vrei 4 coloane egale care se adaptează la lățimea disponibilă, dar nu mai mici de 250px fiecare. Care sintaxă e cea mai bună?
Recapitulare
  • fr = fracțiune din spațiul disponibil (după scăderea dimensiunilor fixe)
  • 1fr 2fr 1fr = proporții (1/4, 2/4, 1/4)
  • repeat(N, valoare) = scriere scurtă pentru N coloane identice
  • minmax(min, max) = dimensiune cu limite
  • minmax(0, 1fr) = 1fr care nu depășește container-ul
  • Combinație standard: repeat(N, minmax(250px, 1fr))
Modulul 5 · Lecția 48 min citire

auto-fit & auto-fill

Cea mai elegantă linie de cod din CSS modern. O singură declarație Grid care transformă automat orice galerie într-una responsive — fără media queries, fără JavaScript. Odată ce o cunoști, o folosești peste tot.

Problema

Cu repeat(4, 1fr) ai mereu 4 coloane, indiferent de ecran. Pe telefon, cardurile devin ilizibil de înguste. Vrei ca numărul de coloane să se schimbe automat în funcție de spațiu.

Soluția — auto-fit

.galerie {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

Un singur rând, efect dramatic:

  • Pe desktop lat (1200px): 4 coloane
  • Pe tabletă (800px): 2-3 coloane
  • Pe telefon (375px): 1 coloană

Totul fără o singură media query.

Cum funcționează

auto-fit înlocuiește numărul din repeat(). În loc să specifici „4 coloane", spui „cât încap, fiecare minim 280px".

Browser-ul calculează: „container-ul e 1200px, minus gap-uri, am 1184px disponibili. Pot încăpea 4 coloane de 280px fiecare (total 1120px), rămân 64px → le distribui între coloane, fiecare crește la 296px."

Pe un container mai mic (600px): „pot încăpea 2 coloane de 280px, le distribui restul → fiecare devine 290px."

auto-fit vs auto-fill

Două cuvinte, diferență subtilă dar importantă.

auto-fit — coloanele existente cresc să umple spațiul disponibil.

auto-fill — dacă nu sunt destule items, rămân coloane goale.

/* 3 carduri, container 1200px, minmax(280px, 1fr) */

.auto-fit {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  /* 3 carduri, fiecare ~390px (umplu containerul) */
}

.auto-fill {
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  /* 3 carduri de 280px + spațiu gol pentru ce ar încăpea */
}
Când folosești care

Pentru galerii normale (carduri de produse, articole, etc.), folosește auto-fit. E ce vrea 90% din lume — items-urile umplu spațiul natural.

auto-fill e util rar — când vrei ca cardurile să rămână la dimensiunea minimă specificată chiar și cu spațiu rămas. De exemplu, o listă cu grid-uri de dimensiune fixă unde „mai multe ar fi încăput".

Regula: când nu ești sigur, folosește auto-fit.

Valoarea magică — minmax + auto-fit

Combinația repeat(auto-fit, minmax(X, 1fr)) e pattern-ul pe care-l vei folosi cel mai des în toată cariera ta CSS:

/* Carduri de produse — minim 280px */
.products {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

/* Galerii foto — minim 200px */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
}

/* Stat cards mici — minim 150px */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1rem;
}

Singura variabilă: dimensiunea minimă. Restul rămâne identic.

Exemplu explicat
Dashboard cu statistici responsive
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  padding: 1.5rem;
}

.stat-card {
  background: white;
  padding: 1.5rem;
  border-radius: 10px;
  text-align: center;
}

.stat-value {
  font-size: 2.5rem;
  font-weight: 500;
}

.stat-label {
  color: #5c564b;
  font-size: 0.875rem;
}
<div class="stats-grid">
  <div class="stat-card"><div class="stat-value">1,247</div><div class="stat-label">Utilizatori</div></div>
  <div class="stat-card"><div class="stat-value">€48K</div><div class="stat-label">Revenue</div></div>
  <div class="stat-card"><div class="stat-value">92%</div><div class="stat-label">Satisfacție</div></div>
  <div class="stat-card"><div class="stat-value">3.2s</div><div class="stat-label">Timp mediu</div></div>
</div>
Pe desktop: 4 cards pe un rând. Pe tabletă: 2 pe un rând, 2 pe al doilea. Pe telefon: stivă verticală. Niciodată cards mai înguste de 200px. Zero media queries.
Pattern recognition
Vrei o galerie de carduri care se adaptează automat la ecran: pe desktop 4 pe rând, pe mobil 1. Fiecare card minim 250px. Care e sintaxa?
Recapitulare
  • repeat(auto-fit, minmax(X, 1fr)) = grid responsive fără media queries
  • auto-fit = items-urile existente umplu spațiul
  • auto-fill = lasă spațiu gol pentru items care ar încăpea (rar folosit)
  • Folosește auto-fit pentru 95% din cazuri
  • Singurul lucru de ajustat: valoarea minimă (200px, 280px, etc.)
  • Un singur rând de CSS înlocuiește 3-4 media queries
Modulul 5 · Lecția 59 min citire

Plasarea items-urilor

Până acum, Grid-ul plasa automat fiecare item într-o celulă, în ordine. Dar Grid îți permite și control explicit — să spui „cardul ăsta ocupă 2 coloane și 3 rânduri". Aici Grid devine cu adevărat puternic.

Sistemul de linii

Grid numerotează liniile — nu celulele, ci liniile dintre ele:

/* Pentru 3 coloane, sunt 4 linii de coloană (1, 2, 3, 4) */
/* Linia 1 e la stânga de tot */
/* Linia 4 e la dreapta de tot */

.layout {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

Similar pentru rânduri — dacă ai 3 rânduri, sunt 4 linii (1 sus, 4 jos).

grid-column și grid-row

Pe un item, spui de la ce linie pornește și la ce linie se termină:

.hero {
  grid-column: 1 / 4;
  /* pornește la linia 1, se termină la linia 4 */
  /* adică se întinde pe toate 3 coloanele */
}

Valori negative pentru „ultima linie"

-1 înseamnă „ultima linie", indiferent câte coloane ai:

.full-width {
  grid-column: 1 / -1;
  /* de la prima la ultima linie = ocupă tot rândul */
}

Pattern foarte folosit pentru header, footer, secțiuni full-width într-un grid.

span — alternativă mai lizibilă

În loc de linii explicite, poți spune „ocupă N coloane":

.item {
  grid-column: span 2; /* ocupă 2 coloane, pornind de unde ar fi natural */
}

.card-mare {
  grid-column: span 2;
  grid-row: span 2;
  /* 2x2 — cel mai mare card din grid */
}

Pentru cele mai multe cazuri, span e mai intuitiv decât să numeri linii.

Plasarea explicită

.layout {
  display: grid;
  grid-template-columns: 240px 1fr 300px;
  grid-template-rows: auto 1fr auto;
}

.header {
  grid-column: 1 / -1;  /* toate coloanele */
  grid-row: 1;
}

.sidebar {
  grid-column: 1;
  grid-row: 2;
}

.main {
  grid-column: 2;
  grid-row: 2;
}

.panel {
  grid-column: 3;
  grid-row: 2;
}

.footer {
  grid-column: 1 / -1;
  grid-row: 3;
}
Galerii cu items de dimensiuni diferite

Unul dintre cele mai „wow" pattern-uri din CSS — mozaic cu items de dimensiuni diferite:

.mozaic {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}

.item-mare {
  grid-column: span 2;
  grid-row: span 2;
}

.item-lat {
  grid-column: span 2;
}

Items-urile normale ocupă 1 celulă. Cele mari ocupă 2x2. Cele late ocupă 2x1. Grid-ul le aranjează automat să umple spațiul.

Alinierea în interiorul celulei

Dacă un item e mai mic decât celula lui, îl poți alinia similar cu Flexbox:

.item {
  justify-self: center; /* orizontal */
  align-self: center;   /* vertical */

  /* Sau pe container, pentru toate items-urile */
}

.grid {
  justify-items: center;
  align-items: center;
}
Exemplu explicat
Dashboard cu hero mare
.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 200px;
  gap: 1rem;
  padding: 1rem;
}

.widget-hero {
  grid-column: span 2;
  grid-row: span 2;
  /* un widget mare 2x2 */
}

.widget-wide {
  grid-column: span 2;
  /* un widget lat 2x1 */
}

.widget {
  /* normal — 1x1 */
}
Într-un dashboard, statisticile mici sunt 1x1, graficul principal e 2x2, tabelele late sunt 2x1. Grid aranjează totul ca tetris, umplând spațiile disponibile.
Plasează corect
Ai un grid cu 3 coloane. Vrei ca un header să se întindă peste toate 3 coloanele. Care e sintaxa corectă?
grid-column: 1 / -1 (de la prima la ultima) sau grid-column: 1 / 4 (de la linia 1 la linia 4, pentru 3 coloane sunt 4 linii). Varianta A are eroare — merge de la linia 1 la linia 3, adică doar 2 coloane. grid-columns nu există, width nu influențează grid." data-msg-wrong="Nu chiar. Răspunsul corect e B. span 3 = ocupă 3 coloane. Varianta A are o eroare: pentru 3 coloane ai 4 linii, deci trebuie 1 / 4.">
Recapitulare
  • Grid numerotează liniile între coloane/rânduri, nu celulele
  • grid-column: 1 / 4 = de la linia 1 la linia 4
  • grid-column: 1 / -1 = întinde pe toate coloanele
  • grid-column: span 2 = ocupă 2 coloane (de unde ar fi natural)
  • grid-row funcționează identic pentru rânduri
  • Galerii mozaic: combină span pe coloane și rânduri pentru items de dimensiuni diferite
Modulul 5 · Lecția 68 min citire

Named grid areas

Cea mai poetică feature din tot CSS-ul. Desenezi layout-ul cu cuvinte, în textul CSS-ului. Chiar și persoane care nu știu CSS pot înțelege ce face. O să vezi.

Ideea

În loc să spui „header-ul merge de la linia 1 la linia 4, sidebar-ul de la linia 1 la linia 2 pe rândul 2", dai nume zonelor și desenezi harta.

.page {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header  header  header"
    "sidebar main    panel"
    "footer  footer  footer";
}

.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.panel   { grid-area: panel; }
.footer  { grid-area: footer; }

Citești CSS-ul și imediat vezi layout-ul: header sus, sidebar+main+panel la mijloc, footer jos. Fără să numeri linii.

Sintaxa grid-template-areas

Fiecare string e un rând. Fiecare cuvânt din string e o celulă.

grid-template-areas:
  "a b c"
  "d e f";
/* 2 rânduri x 3 coloane, 6 zone */

Îmbinarea celulelor

Repetă numele ca să îmbini celule adiacente:

grid-template-areas:
  "header header header"
  "nav    main   main"
  "footer footer footer";

„header" apare de 3 ori pe rândul 1 → o singură zonă care ocupă toate 3 coloanele. „main" apare de 2 ori pe rândul 2 → zonă care ocupă 2 coloane.

Celule goale cu punct

Folosește . pentru celule fără conținut:

grid-template-areas:
  "header header header"
  "nav    main   ."
  "footer footer footer";
/* celula dreapta-sus rămâne goală */

Plasarea items-urilor

Folosești grid-area cu numele zonei:

.main {
  grid-area: main;
  /* se plasează în zona „main" definită în grid-template-areas */
}
Reguli pentru grid-template-areas
  • Fiecare rând e un string separat (cu ghilimele)
  • Toate rândurile trebuie să aibă același număr de celule
  • Zonele trebuie să fie dreptunghiulare (nu forme ciudate)
  • Zonele trebuie să fie conectate (nu poți avea „header" în două zone separate)

De ce e atât de puternic

Imaginează-ți că vrei să muți sidebar-ul de la stânga la dreapta. Fără areas:

/* trebuie să schimbi grid-column pe fiecare element */
.sidebar { grid-column: 3; } /* era 1 */
.main { grid-column: 1 / 3; } /* era 2 / 4 */
.panel { grid-column: 1; } /* era 3 */

Cu areas, schimbi doar harta:

grid-template-areas:
  "header  header  header"
  "panel   main    sidebar"   /* doar asta s-a schimbat */
  "footer  footer  footer";

/* CSS-ul pe items rămâne identic */

Rearanjare pentru mobil

Combinat cu media queries, areas îți permit să rearanjezi complet layout-ul pentru ecrane mici:

/* Desktop */
.page {
  grid-template-columns: 240px 1fr 300px;
  grid-template-areas:
    "header  header  header"
    "sidebar main    panel"
    "footer  footer  footer";
}

/* Mobile */
@media (max-width: 768px) {
  .page {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "sidebar"
      "panel"
      "footer";
  }
}

Aceleași zone, ordine diferită, totul stivuit. Fără să atingi HTML-ul.

Exemplu explicat
Layout complet de blog
<div class="site">
  <header class="site-header">Logo și nav</header>
  <nav class="site-sidebar">Categorii</nav>
  <main class="site-main">Articole</main>
  <aside class="site-aside">Populare</aside>
  <footer class="site-footer">Copyright</footer>
</div>
.site {
  display: grid;
  grid-template-columns: 200px 1fr 240px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header  header  header"
    "sidebar main    aside"
    "footer  footer  footer";
  gap: 1.5rem;
  min-height: 100vh;
}

.site-header { grid-area: header; }
.site-sidebar { grid-area: sidebar; }
.site-main { grid-area: main; }
.site-aside { grid-area: aside; }
.site-footer { grid-area: footer; }
Uită-te la grid-template-areas — vezi layout-ul paginii scris în cuvinte, în CSS. Auto-documentat.
Desenează layout-ul
Ai un layout cu header sus (întins pe toată lățimea), main central, sidebar dreapta, și footer jos. Care grid-template-areas e corect?
Recapitulare
  • grid-template-areas = desenezi layout-ul cu cuvinte
  • Fiecare rând e un string separat în ghilimele
  • Repetă numele pentru a îmbina celule
  • Folosește . pentru celule goale
  • Pe items: grid-area: nume-zona
  • Rearanjare pentru mobil: schimbi doar harta în media query
Modulul 5 · Lecția 79 min citire

Pattern-uri comune

Ai toate uneltele Grid-ului. Hai să vedem cele mai folosite pattern-uri — cele pe care le vei reutiliza în fiecare proiect de aici încolo.

Pattern 1: Galerie auto-responsive

Pattern-ul pe care îl vei folosi cel mai des:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

Carduri produse, articole de blog, imagini — oricând ai items similare de aranjat.

Pattern 2: Layout de pagină complet (Holy Grail)

.page {
  display: grid;
  grid-template-columns: 240px 1fr 300px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header  header  header"
    "sidebar main    panel"
    "footer  footer  footer";
  min-height: 100vh;
}

.page-header { grid-area: header; }
.page-sidebar { grid-area: sidebar; }
.page-main { grid-area: main; }
.page-panel { grid-area: panel; }
.page-footer { grid-area: footer; }

Pattern 3: Imagine și text alături

Secțiune „feature" cu imagine pe o parte și text pe cealaltă:

.feature {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: center;
}

.feature-reverse {
  direction: rtl; /* simplu trick pentru a inversa ordinea */
}
.feature-reverse > * {
  direction: ltr;
}

Pattern 4: Cards cu înălțimi diferite (mozaic)

.mosaic {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 200px;
  gap: 1rem;
}

.mosaic-big {
  grid-column: span 2;
  grid-row: span 2;
}

.mosaic-wide {
  grid-column: span 2;
}

Cel mai eficient mod de a face layout-uri „Pinterest-style" simple.

Pattern 5: Formular în două coloane

.form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem 1.5rem;
}

.form-field-full {
  grid-column: 1 / -1; /* câmp full-width */
}

Prenume și nume unul lângă altul, apoi email pe toată lățimea. Clar și ordonat.

Pattern 6: Centrare perfectă

Grid poate centra și mai scurt decât Flexbox:

.container {
  display: grid;
  place-items: center;
  min-height: 100vh;
}

place-items: center = align-items: center + justify-items: center. O linie, totul centrat.

Pattern 7: Grid cu sub-grid (nested)

Grid în Grid — fiecare card e el însuși un mini-grid:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-template-rows: 200px auto 1fr auto;
  /* imagine, titlu, descriere care crește, footer */
}
Grid sau Flexbox în componente?

Principiu: Grid pentru layout, Flexbox pentru componente.

  • Grid pentru pagină: header, sidebar, main — da
  • Grid pentru galerie de cards — da
  • Grid pentru footer-ul unui card (preț + buton) — prefer Flexbox
  • Grid pentru navbar — Flexbox e mai natural

Grid e „plasare în celule definite". Flexbox e „aranjare într-o linie". Alege în funcție de intenție.

Exemplu explicat
Dashboard complet combinând pattern-uri
.dashboard {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 1fr;
  grid-template-areas:
    "sidebar header"
    "sidebar main";
  min-height: 100vh;
}

.sidebar { grid-area: sidebar; }
.header  { grid-area: header; }
.main    { grid-area: main; overflow: auto; padding: 2rem; }

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}

.content-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
}
Trei nivele de Grid: dashboard-ul mare (areas), stat cards (auto-fit), content cu 2 coloane. Fiecare nivel rezolvă o problemă specifică cu pattern-ul potrivit.
Alege pattern-ul
Faci o pagină produse cu mulți cards. Vrei să arate bine pe orice ecran fără media queries. Ce pattern folosești?
Recapitulare
  • Galerie responsive: repeat(auto-fit, minmax(280px, 1fr))
  • Page layout: grid-template-areas cu harta vizuală
  • Imagine lângă text: grid-template-columns: 1fr 1fr
  • Mozaic: grid-column: span N + grid-row: span N
  • Formulare: câmpuri pe 2 coloane + câmpuri full cu grid-column: 1 / -1
  • Centrare: place-items: center
  • Grid pentru layout, Flexbox pentru componente
Modulul 5 · Lecția 815 min citire

Proiect: dashboard

Momentul de glorie al Grid-ului. Construim un dashboard — genul de layout pentru care Grid a fost creat. Sidebar, header, zone de statistici, tabel. Totul aliniat, responsive, fără hack-uri.

Ce construim

Dashboard cu:

  • Sidebar stâng cu navigație
  • Header sus cu titlu și user info
  • 4 cards de statistici
  • O zonă principală cu grafic mare + tabel

Totul construit exclusiv cu Grid și Flexbox, fără media queries.

Pasul 1: HTML-ul

<div class="dashboard">

  <aside class="sidebar">
    <div class="sidebar-brand">
      <div class="brand-mark">S</div>
      <span>Admin</span>
    </div>
    <nav class="sidebar-nav">
      <a href="#" class="active">Overview</a>
      <a href="#">Utilizatori</a>
      <a href="#">Comenzi</a>
      <a href="#">Produse</a>
      <a href="#">Setări</a>
    </nav>
  </aside>

  <header class="top-bar">
    <h1>Overview</h1>
    <div class="user-info">Alin Dragoescu</div>
  </header>

  <main class="content">
    <section class="stats">
      <div class="stat">
        <div class="stat-label">Revenue</div>
        <div class="stat-value">€48,290</div>
        <div class="stat-change positive">+12.5%</div>
      </div>
      <div class="stat">
        <div class="stat-label">Utilizatori</div>
        <div class="stat-value">1,247</div>
        <div class="stat-change positive">+3.2%</div>
      </div>
      <div class="stat">
        <div class="stat-label">Comenzi</div>
        <div class="stat-value">384</div>
        <div class="stat-change negative">-2.1%</div>
      </div>
      <div class="stat">
        <div class="stat-label">Conversie</div>
        <div class="stat-value">3.8%</div>
        <div class="stat-change positive">+0.4%</div>
      </div>
    </section>

    <section class="main-grid">
      <div class="chart-card">
        <h2>Vânzări lunare</h2>
        <div class="chart-placeholder">Grafic</div>
      </div>
      <div class="table-card">
        <h2>Ultimele comenzi</h2>
        <ul class="orders">
          <li><span>#1284</span> <span>€129</span></li>
          <li><span>#1283</span> <span>€82</span></li>
          <li><span>#1282</span> <span>€247</span></li>
          <li><span>#1281</span> <span>€54</span></li>
        </ul>
      </div>
    </section>
  </main>

</div>

Pasul 2: Structura cu Grid Areas

.dashboard {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 64px 1fr;
  grid-template-areas:
    "sidebar top-bar"
    "sidebar content";
  min-height: 100vh;
}

.sidebar { grid-area: sidebar; }
.top-bar { grid-area: top-bar; }
.content { grid-area: content; }

Pasul 3: Sidebar

.sidebar {
  background: var(--color-text);
  color: white;
  padding: 1.5rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0 0.5rem;
  font-family: var(--font-display);
  font-size: 1.25rem;
}

.brand-mark {
  width: 32px;
  height: 32px;
  background: var(--color-accent);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  flex-shrink: 0;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.sidebar-nav a {
  padding: 0.625rem 0.875rem;
  border-radius: 8px;
  color: rgba(255,255,255,0.7);
  font-weight: 500;
  transition: background 150ms, color 150ms;
}

.sidebar-nav a:hover {
  background: rgba(255,255,255,0.08);
  color: white;
}

.sidebar-nav a.active {
  background: var(--color-accent);
  color: white;
}

Pasul 4: Top bar

.top-bar {
  background: var(--color-panel);
  border-bottom: 1px solid var(--color-border);
  padding: 0 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.top-bar h1 {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
}

.user-info {
  padding: 0.5rem 1rem;
  background: var(--color-bg);
  border-radius: 8px;
  font-weight: 500;
  font-size: 0.875rem;
}

Pasul 5: Content

.content {
  padding: 2rem;
  overflow-y: auto;
}

/* Stat cards — auto-fit pentru responsive */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}

.stat {
  background: var(--color-panel);
  padding: 1.25rem;
  border-radius: 12px;
  border: 1px solid var(--color-border);
}

.stat-label {
  color: var(--color-text-soft);
  font-size: 0.875rem;
  margin-bottom: 0.5rem;
}

.stat-value {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  letter-spacing: -0.02em;
  margin-bottom: 0.5rem;
}

.stat-change {
  font-size: 0.8125rem;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: 100px;
  display: inline-block;
}

.stat-change.positive {
  background: #e3efe5;
  color: #2d4d37;
}

.stat-change.negative {
  background: #fbe9e7;
  color: #6b2d1e;
}

Pasul 6: Main grid (grafic + tabel)

.main-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 1.5rem;
}

.chart-card,
.table-card {
  background: var(--color-panel);
  padding: 1.5rem;
  border-radius: 12px;
  border: 1px solid var(--color-border);
}

.chart-card h2,
.table-card h2 {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 400;
  margin-bottom: 1.25rem;
}

.chart-placeholder {
  height: 280px;
  background: var(--color-bg);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-soft);
}

.orders {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.orders li {
  display: flex;
  justify-content: space-between;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--color-border);
}

.orders li:last-child {
  border-bottom: none;
}

.orders li span:first-child {
  color: var(--color-text-soft);
  font-family: monospace;
}

.orders li span:last-child {
  font-weight: 500;
}

Ce face layout-ul special

Trei nivele de Grid:

  1. Dashboard generalgrid-template-areas pentru sidebar+top+content
  2. Stats cardsauto-fit + minmax pentru responsive automat
  3. Main grid2fr 1fr pentru grafic lat + tabel îngust

Flexbox în interior — sidebar-ul, navigarea, fiecare ordin din tabel, top bar-ul. Grid pentru structura mare, Flexbox pentru componente.

Zero media queries — stats se adaptează automat. Pe desktop mic, dashboard-ul se poate strica — ar trebui un media query la 768px care schimbă grid-template-columns pe dashboard și convertește sidebar-ul într-un hamburger. Asta pentru Modulul 6 (Responsive).

Ce nu am făcut aici

Acesta e un exemplu educațional, nu o aplicație reală. Într-un proiect serios:

  • Ai avea hamburger menu pentru sidebar pe mobil
  • Graficul ar fi o bibliotecă reală (Chart.js, D3)
  • Datele ar veni dintr-un backend, nu hardcodate
  • Ai avea accesibilitate pentru sidebar-ul de navigare (ARIA labels)

Dar structura Grid rămâne aceeași. De aia merită să înveți bine fundamentele — le vei folosi peste tot.

Recapitulare
  • Dashboard clasic = Grid areas (sidebar + top + content)
  • Stats cards = auto-fit + minmax pentru responsive
  • Content zone = fr proportions (2fr 1fr)
  • Grid pentru layout mare, Flexbox pentru componente mici
  • Nivele de Grid nested — fiecare nivel rezolvă o problemă
  • Proiectul real are nevoie și de Responsive (Modulul 6 urmează)

🎉 Ai terminat Modulul 5!
Flexbox + Grid = acoperi orice layout din frontend modern. Modulul 6 (Responsive Design) adaugă reglajul fin pentru diferite dispozitive.

Modulul 6 · Lecția 18 min citire

Ce înseamnă responsive

În 2007, site-urile erau făcute pentru desktop. Când a apărut iPhone-ul, toți am început să zoom-uim și să pan-uim pe site-uri rupte. Azi, peste 60% din traficul web e de pe telefon. A face site-uri care arată bine pe orice ecran nu e opțional — e mesaj de bază.

Responsive design — cele 3 principii

1. Layout fluid — elementele se întind și se adaptează la spațiul disponibil. Fără lățimi fixe mari.

2. Imagini flexibile — imaginile nu depășesc containerul, se scalează.

3. Media queries — reguli CSS care se aplică doar în anumite condiții (ex. ecrane mici).

Mobile-first — filozofia modernă

Cândva, developerii scriau CSS pentru desktop și apoi „reparau" pentru mobil. Astăzi, standardul e invers: scrii întâi pentru mobil, apoi adaugi reguli pentru ecrane mai mari.

/* Base styles — pentru mobil */
.card {
  padding: 1rem;
  font-size: 0.875rem;
}

/* Pentru tablete și peste */
@media (min-width: 768px) {
  .card {
    padding: 1.5rem;
    font-size: 1rem;
  }
}

/* Pentru desktop și peste */
@media (min-width: 1200px) {
  .card {
    padding: 2rem;
  }
}

De ce mobile-first:

  • Telefonul e cel mai comun device — proiectezi pentru majoritate
  • Constrângerile te forțează să prioritizezi (ce e esențial pe un ecran mic?)
  • E mai ușor să adaugi complexitate decât să o îndepărtezi
  • Performanța e mai bună — încarci doar ce e necesar pe mobil

Meta viewport — pasul obligatoriu

Pe fiecare pagină HTML, trebuie acest tag în <head>:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Fără el, browser-ul mobil pretinde că ecranul e 980px lat, apoi face zoom-out. Pagina ta arată ca o versiune miniatură. Cu el, pagina folosește lățimea reală a ecranului.

Nu uita meta viewport

Cel mai comun bug de responsive: pagina arată ciudat pe telefon pentru că lipsește <meta name="viewport">. Adaugă-l în fiecare HTML. Noi l-am inclus deja în toate exemplele din modulele anterioare.

Layout care merge pe orice ecran

Multe lucruri pe care le-ai învățat deja sunt intrinsec responsive:

  • Grid cu auto-fit + minmax (Modulul 5) — adaptare automată fără media queries
  • Flexbox cu flex-wrap: wrap (Modulul 4) — items-urile sar pe rânduri noi
  • max-width în loc de width (Modulul 3) — elementele nu depășesc ecranul
  • Unități relative (rem, em, %) (Modulul 2) — scalare naturală

Dacă ai urmat lecțiile, site-urile tale sunt deja parțial responsive. Media queries adaugă ajustări fine.

Exemplu explicat
Structură mobile-first simplă
/* BASE — stilurile pentru mobil (ecrane mici) */
.hero {
  padding: 2rem 1rem;
  text-align: center;
}

.hero h1 {
  font-size: 2rem;
  line-height: 1.2;
}

.hero p {
  font-size: 1rem;
}

/* De la tabletă în sus, totul se mărește */
@media (min-width: 768px) {
  .hero {
    padding: 4rem 2rem;
  }
  .hero h1 {
    font-size: 3rem;
  }
  .hero p {
    font-size: 1.125rem;
  }
}

/* De la desktop, și mai generos */
@media (min-width: 1200px) {
  .hero h1 {
    font-size: 4rem;
  }
}
Baza funcționează pe telefon. Pe tabletă, padding-ul și fontul cresc. Pe desktop, titlul devine și mai mare. Creșterea e organică, nu salturi bruște.
Strategia potrivită
Care e abordarea recomandată azi pentru a face un site responsive?
Recapitulare
  • Responsive = site care arată bine pe orice ecran (mobil, tabletă, desktop)
  • Cele 3 principii: layout fluid + imagini flexibile + media queries
  • Mobile-first = scrii întâi pentru ecranul cel mai mic, apoi adaugi pentru mai mari
  • <meta name="viewport"> e obligatoriu
  • Ce ai învățat deja (Grid, Flexbox, max-width) te duce mult
Modulul 6 · Lecția 29 min citire

Media queries

Media queries sunt „dacă-atunci"-urile CSS-ului. „Dacă ecranul are cel puțin 768px, atunci aplică stilurile astea." O sintaxă simplă, un efect enorm asupra cum arată site-urile tale.

Sintaxa de bază

@media (condiție) {
  /* reguli CSS care se aplică doar când condiția e adevărată */
  .clasa {
    proprietate: valoare;
  }
}

Cele mai folosite condiții

min-width — ecran de cel puțin X

@media (min-width: 768px) {
  /* se aplică pe ecrane de 768px și mai mari */
  .header {
    padding: 2rem;
  }
}

Folosit în mobile-first: scrii baza pentru mobil, apoi „crești" la ecrane mai mari.

max-width — ecran de cel mult X

@media (max-width: 767px) {
  /* se aplică pe ecrane de 767px sau mai mici */
  .sidebar {
    display: none;
  }
}

Util pentru ajustări specifice mobilului. Dar în mobile-first, rar folosit — preferi să nu aplici sidebar-ul deloc până când nu e spațiu.

Intervale — combinații

@media (min-width: 768px) and (max-width: 1199px) {
  /* doar pe tabletă */
}

@media (min-width: 1200px) {
  /* desktop */
}

Orientarea ecranului

@media (orientation: landscape) {
  /* telefon ținut pe orizontal */
}

@media (orientation: portrait) {
  /* telefon ținut pe vertical */
}

Preferința utilizatorului pentru dark mode

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1a1814;
    --color-text: #e8e3d5;
  }
}

Dacă utilizatorul are sistemul setat pe dark mode, activezi automat paleta întunecată. Zero JavaScript.

Preferința pentru mișcare redusă

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}

Unii utilizatori au sensibilitate la animații (vertigo, tulburări vestibulare). Sistemul lor operațional le permite să dezactiveze animațiile — tu trebuie să respecți preferința asta.

Anatomia unui site responsive tipic

/* ========== BASE (Mobile) ========== */
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

.nav-links {
  display: none; /* hamburger menu pe mobil */
}

/* ========== TABLET (768px+) ========== */
@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem;
  }

  .nav-links {
    display: flex; /* meniu vizibil pe tabletă */
  }
}

/* ========== DESKTOP (1200px+) ========== */
@media (min-width: 1200px) {
  .grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 2rem;
  }
}
Folosește min-width, nu max-width

În mobile-first, tot ce crești trebuie să fie min-width. Dacă amesteci min-width și max-width, cascada CSS devine confuză.

Regulă: scrii baza pentru mobil. Fiecare breakpoint adaugă doar ce se schimbă la ecrane mai mari. Simplu, predictibil.

Breakpoints — în ce unitate?

Convenție modernă: folosește em sau rem, nu px.

/* Preferat */
@media (min-width: 48em) { /* 768px la font-size default */ }
@media (min-width: 75em) { /* 1200px */ }

/* Ok */
@media (min-width: 768px) { }

De ce em: dacă utilizatorul mărește dimensiunea fontului (din preferințe browser), breakpoint-urile se ajustează proporțional. Cu px, rămân fixe indiferent de zoom.

În practică, multe proiecte folosesc px pentru că e mai ușor de citit. Ambele sunt acceptate.

Exemplu explicat
Navbar care devine hamburger pe mobil
/* Base (mobile) */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
}

.nav-links {
  display: none; /* ascuns pe mobil */
}

.hamburger {
  display: block; /* vizibil pe mobil */
  padding: 0.5rem;
}

/* Desktop */
@media (min-width: 768px) {
  .nav-links {
    display: flex;
    gap: 2rem;
  }

  .hamburger {
    display: none; /* ascuns pe desktop */
  }
}
Pe mobil: hamburger vizibil, link-uri ascunse. Pe desktop: link-urile apar, hamburger-ul dispare. Doar CSS — fără JavaScript, doar structura e gata pentru când adaugi JS.
Strategia media query
Scrii mobile-first. Vrei ca un sidebar care nu apare pe telefoane să se afișeze începând cu 900px. Care e media query-ul corect?
Recapitulare
  • @media (condiție) { ... } = CSS condițional
  • Mobile-first folosește min-width, nu max-width
  • Combinație cu and: (min-width: 768px) and (max-width: 1199px)
  • Alte media queries utile: prefers-color-scheme, prefers-reduced-motion, orientation
  • em e mai accesibil decât px pentru breakpoints
Modulul 6 · Lecția 38 min citire

Breakpoints — unde și cum

„La ce lățime ar trebui să-mi pun media queries?" Întrebarea bună. Răspunsul modern: nu la dimensiuni de telefoane specifice — la punctele unde design-ul tău se rupe.

Abordarea veche — pe device

Înainte, breakpoints-urile urmăreau device-uri populare: iPhone 320px, iPad 768px, desktop 1024px. Problema: prea multe device-uri cu dimensiuni diferite. Telefonul mediu azi e între 375 și 428px lățime, iar tablete există între 600 și 1200px.

Abordarea modernă — pe design

Adaugi un breakpoint când design-ul tău are nevoie. Iei layout-ul de la mobile și crești lățimea browser-ului. Când începe să arate rău (texte prea largi, prea mult spațiu gol, layout prea înghesuit) — ăla e breakpoint-ul.

Mulți au 0 breakpoints. Alții au 6. Nu există un număr magic.

Breakpoints comune

Cu toate astea, există valori convenționale care merg pentru majoritatea proiectelor:

:root {
  /* Design tokens pentru breakpoints */
  --bp-sm: 640px;   /* telefoane mari / mini-tablete */
  --bp-md: 768px;   /* tablete */
  --bp-lg: 1024px;  /* tablete mari / laptop-uri */
  --bp-xl: 1280px;  /* desktop */
  --bp-2xl: 1536px; /* desktop mare */
}

Notă: CSS custom properties nu pot fi folosite direct în media queries. Sunt pentru referință. Breakpoint-urile efective:

/* Tablete și peste */
@media (min-width: 768px) { }

/* Laptop și peste */
@media (min-width: 1024px) { }

/* Desktop lat și peste */
@media (min-width: 1280px) { }

Breakpoints pentru Tailwind (ca referință)

Frameworks-urile populare standardizează breakpoints. Tailwind CSS, cel mai folosit framework utility-first:

  • sm: 640px
  • md: 768px
  • lg: 1024px
  • xl: 1280px
  • 2xl: 1536px

Dacă lucrezi cu ei, ai un punct de referință. Dacă nu, te poți inspira.

Strategia cu puține breakpoints

Mulți designeri moderni folosesc doar 2 breakpoints:

  • Mobile (base, 0-767px)
  • Tablet+ (768px și peste)
  • Desktop (1200px și peste) — opțional

Cu layout-uri flexibile (Grid auto-fit, Flexbox wrap), adesea ăsta e tot ce ai nevoie. Extra breakpoints adaugă complexitate fără valoare.

Nu pune media queries în multe locuri

Anti-pattern: scrii media queries la finalul fiecărui component. Rezultat: sute de media queries împrăștiate.

/* Rău — media queries împrăștiate */
.card { padding: 1rem; }
@media (min-width: 768px) { .card { padding: 2rem; } }

.button { padding: 0.5rem; }
@media (min-width: 768px) { .button { padding: 1rem; } }

Mai bine: grupează pe breakpoint.

.card { padding: 1rem; }
.button { padding: 0.5rem; }

@media (min-width: 768px) {
  .card { padding: 2rem; }
  .button { padding: 1rem; }
}

Testarea breakpoints-urilor

În browser, deschide DevTools (F12), activează modul responsive (Ctrl+Shift+M în Chrome/Firefox). Acolo poți:

  • Selecta dimensiuni preset (iPhone, iPad, Desktop)
  • Glisa lent să vezi unde se rupe design-ul
  • Nota acele puncte ca breakpoints noi

Breakpoint-uri pentru componente specifice

Uneori, un component are nevoie de un breakpoint care nu coincide cu cel global:

/* Un navbar care se rupe la 900px */
.navbar {
  display: flex;
  flex-direction: column;
}

@media (min-width: 900px) {
  .navbar {
    flex-direction: row;
  }
}

E ok. Nu trebuie ca toate breakpoints-urile să fie la valori globale. Poți avea unele la 900px, altele la 768px, altele la 1100px.

Exemplu explicat
Strategia minimă viabilă
/* === BASE (mobile) === */
.container {
  padding: 1rem;
}

.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

.title {
  font-size: 2rem;
}

/* === TABLET+ (768px) === */
@media (min-width: 768px) {
  .container {
    padding: 2rem;
  }

  .grid {
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 2rem;
  }

  .title {
    font-size: 3rem;
  }
}

/* === DESKTOP+ (1200px) === */
@media (min-width: 1200px) {
  .container {
    padding: 3rem;
    max-width: 1200px;
    margin: 0 auto;
  }

  .title {
    font-size: 4rem;
  }
}
Doar 2 breakpoints. Grid-ul auto-fit face o mare parte din munca responsive singur. Media queries doar pentru ajustări fine de tipografie și spațiere.
Filosofia potrivită
Când ar trebui să adaugi un breakpoint nou?
Recapitulare
  • Breakpoints-uri bune = acolo unde design-ul tău se rupe, nu la dimensiuni de device
  • Convenție comună: 768px (tabletă), 1024px (laptop), 1280px (desktop)
  • Mulți designeri folosesc doar 2-3 breakpoints
  • Grupează media queries pe breakpoint, nu pe component
  • Component-specific breakpoints sunt ok (900px pentru navbar, 768px pentru grid)
Modulul 6 · Lecția 49 min citire

Viewport units & clamp()

Media queries sunt foarte folositoare, dar sunt sărituri bruște — la 768px pur-și-simplu se schimbă totul. Uneori vrei ceva mai fluid: tipografie care crește gradual cu ecranul, spații care se ajustează natural. Aici intră viewport units și clamp().

Viewport units — recapitulare

Unitățile legate de dimensiunea viewport-ului browser-ului:

  • vw — 1% din lățimea viewport-ului
  • vh — 1% din înălțimea viewport-ului
  • vmin — 1% din cea mai mică dimensiune
  • vmax — 1% din cea mai mare dimensiune
.hero {
  min-height: 100vh; /* tot ecranul pe vertical */
}

.title {
  font-size: 5vw; /* scalează cu lățimea ecranului */
}

Dynamic viewport units — moderne

Pe mobil, bara de URL a browser-ului apare și dispare când scrollezi. Asta schimbă înălțimea viewport-ului. 100vh se referă la cea mare (fără bara), ceea ce cauzează probleme.

Soluția modernă:

  • dvh — dynamic viewport height (se ajustează cu bara)
  • svh — small viewport height (cu bara vizibilă)
  • lvh — large viewport height (fără bara)
.hero {
  min-height: 100dvh; /* funcționează corect pe mobil modern */
}

Problema cu vw direct

Dacă setezi font-size: 5vw, pe un ecran de 1920px ai 96px (uriaș), pe un ecran de 375px ai 18.75px (acceptabil). Variația e prea mare.

Plus, fontul scalează la infinit. Pe un monitor 4K, titlul devine absurd. Avem nevoie de limite.

clamp() — dimensiunea cu limite

clamp(min, preferat, max) îți dă o valoare care:

  • Nu e niciodată mai mică decât min
  • Nu e niciodată mai mare decât max
  • Între acestea, folosește preferat
.title {
  font-size: clamp(2rem, 5vw, 4rem);
}

Traducere: „cel puțin 2rem (32px), cel mult 4rem (64px), iar între, 5% din lățimea ecranului".

De ce e revoluționar

Cu clamp, tipografia se scalează continuu — fără salturi la breakpoints. Pe orice ecran, titlul are exact dimensiunea potrivită.

Înlocuiește complet 2-3 media queries pentru font-size:

/* Versiunea veche — 3 media queries */
.title { font-size: 2rem; }
@media (min-width: 768px) { .title { font-size: 3rem; } }
@media (min-width: 1200px) { .title { font-size: 4rem; } }

/* Versiunea modernă — o linie */
.title {
  font-size: clamp(2rem, 5vw, 4rem);
}

clamp pentru padding și spacing

Merge și pe alte proprietăți:

.section {
  padding: clamp(2rem, 5vw, 6rem) clamp(1rem, 3vw, 3rem);
  /* padding vertical între 2-6rem, orizontal între 1-3rem */
}

.container {
  max-width: clamp(320px, 90vw, 1200px);
  /* lățime între 320-1200px, preferat 90% din viewport */
}
Fluid vs responsive

Există două filosofii:

  • Responsive — media queries, salturi la breakpoints
  • Fluid — clamp, scalare continuă fără salturi

Azi se combină ambele. Tipografie și spacing fluid cu clamp. Layout cu media queries când trebuie să schimbi structura (ex. coloane).

Calcularea valorilor pentru clamp

Cum alegi min, preferat și max?

  • min = dimensiunea pe cel mai mic ecran pe care-l suporți (mobil mic)
  • max = dimensiunea pe cel mai mare ecran practic (desktop mare)
  • preferat = de obicei o expresie vw (3-8vw pentru fonturi)

Pentru titluri hero, o combinație tipică: clamp(2rem, 5vw, 5rem). Pentru body text: clamp(1rem, 1vw + 0.5rem, 1.25rem).

Combinare cu calc()

Pentru preferat, poți folosi expresii calc:

.title {
  font-size: clamp(2rem, 1.5rem + 3vw, 5rem);
  /* pornește cu 1.5rem + o scalare cu 3% din viewport */
}

Asta dă un punct de pornire mai stabil, apoi scalează natural. E formula folosită de majoritatea designerilor pentru tipografie fluidă „gustoasă".

Exemplu explicat
Tipografie fluidă completă
:root {
  /* Tokens pentru tipografie fluidă */
  --fs-xs: clamp(0.75rem, 0.7rem + 0.3vw, 0.875rem);
  --fs-sm: clamp(0.875rem, 0.8rem + 0.4vw, 1rem);
  --fs-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fs-lg: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
  --fs-xl: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
  --fs-2xl: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
  --fs-hero: clamp(2.5rem, 1.5rem + 5vw, 6rem);
}

body { font-size: var(--fs-base); }
h1 { font-size: var(--fs-hero); }
h2 { font-size: var(--fs-2xl); }
h3 { font-size: var(--fs-xl); }
Sistem de tipografie care scalează continuu. Pe orice ecran, totul are o relație proporțională. Fără niciun media query. Pot fi folosite peste tot ca variabile CSS.
Aplică clamp
Vrei titlul hero să fie cel puțin 32px pe mobil, să scaleze cu lățimea, dar să nu treacă de 80px pe ecrane mari. Care e expresia corectă?
Recapitulare
  • vw, vh scalează cu viewport-ul (dar la infinit)
  • dvh e înălțimea dinamică (ajustare pentru bara URL mobil)
  • clamp(min, preferat, max) = dimensiune cu limite superioară și inferioară
  • Tipografie fluidă = clamp pentru font-size, înlocuiește 2-3 media queries
  • Formula bună: clamp(Xrem, Yrem + Zvw, Wrem)
  • Combină responsive (media queries pentru layout) cu fluid (clamp pentru dimensiuni)
Modulul 6 · Lecția 58 min citire

Imagini responsive

O imagine de 2400x1600 pe telefon e 3MB descărcați degeaba. Pe desktop, o imagine de 400x300 arată ca dintr-un joc din 1998. Responsive nu înseamnă doar „text care se adaptează" — imaginile au propriile tehnici.

Regula de bază — max-width 100%

img {
  max-width: 100%;
  height: auto;
  display: block;
}

Asta previne ca imaginile să depășească container-ul. height: auto menține proporțiile. Aplicat la toate imaginile, rezolvă 80% din problemele responsive.

object-fit — umplere controlată

Dacă dai unei imagini dimensiuni fixe (pentru cards uniforme), ai nevoie de object-fit:

.card-img {
  width: 100%;
  height: 200px;
  object-fit: cover; /* umple, taie ce depășește */
  /* object-fit: contain;  încape toată, poate lăsa spațiu */
  /* object-fit: fill;     deformează să umple */
}

object-fit: cover e cel mai folosit — similar cu background-size: cover, dar pentru elemente <img>.

srcset — imagini diferite pe ecrane diferite

În loc să serveşti o singură imagine tuturor, specifici mai multe versiuni:

<img
  src="foto-400.jpg"
  srcset="foto-400.jpg 400w,
          foto-800.jpg 800w,
          foto-1600.jpg 1600w"
  sizes="(max-width: 640px) 100vw, 50vw"
  alt="Descriere foto"
>

Browser-ul alege imaginea cea mai potrivită în funcție de:

  • Lățimea viewport-ului
  • Densitatea pixelilor ecranului (retina)
  • Condițiile de rețea

Atributul sizes explicat

Spune browser-ului cât spațiu va ocupa imaginea în layout:

sizes="(max-width: 640px) 100vw, 50vw"
/* Pe ecrane sub 640px, imaginea ocupă 100% din viewport.
   Altfel, ocupă 50%. */

Browser-ul folosește asta să aleagă ce dimensiune să descarce. Dacă imaginea va ocupa 50% din ecran, nu descărca versiunea full-width.

Picture — art direction

Uneori vrei crop-uri diferite pentru ecrane diferite, nu doar dimensiuni. De exemplu: pe desktop arăți o imagine lată (16:9), pe mobil una pătrată (1:1):

<picture>
  <source
    media="(max-width: 640px)"
    srcset="hero-mobil.jpg">
  <source
    media="(min-width: 641px)"
    srcset="hero-desktop.jpg">
  <img src="hero-desktop.jpg" alt="Descriere">
</picture>

Formatele moderne — WebP și AVIF

JPG și PNG sunt vechi și mari. Formatele moderne:

  • WebP — 25-35% mai mic decât JPG, suportat peste tot
  • AVIF — 50% mai mic decât JPG, suport în creștere
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Descriere">
</picture>

Browser-ul încearcă AVIF primul. Dacă nu suportă, încearcă WebP. Dacă nu suportă nici aia, JPG-ul ca fallback. Utilizatorii cu browsere moderne primesc fișiere mai mici.

Lazy loading — gratuit și important

Imaginile de sub „fold" (nu vizibile la încărcare) pot fi încărcate doar când utilizatorul scroll-ează aproape:

<img src="foto.jpg" alt="..." loading="lazy">

O linie. Browser-ul face treaba. Site-ul se încarcă semnificativ mai rapid pe pagini cu multe imagini (galerii, blog-uri).

Nu lazy-load prima imagine de deasupra fold-ului (hero) — ar vedea un moment de întârziere.

Dimensiuni explicite — previn layout shift

Specifică mereu width și height în HTML:

<img
  src="foto.jpg"
  width="800"
  height="600"
  alt="Descriere"
>

Browser-ul rezervă spațiul corect înainte să încarce imaginea. Fără dimensiuni, conținutul „sare" când se încarcă imaginea — experiență enervantă.

Combinat cu max-width: 100%; height: auto; în CSS, imaginea rămâne proporțională la orice dimensiune.

Exemplu explicat
Imagine responsive completă, production-ready
<picture>
  <source
    type="image/avif"
    srcset="hero-400.avif 400w,
            hero-800.avif 800w,
            hero-1600.avif 1600w"
    sizes="(max-width: 640px) 100vw, 80vw"
  >
  <source
    type="image/webp"
    srcset="hero-400.webp 400w,
            hero-800.webp 800w,
            hero-1600.webp 1600w"
    sizes="(max-width: 640px) 100vw, 80vw"
  >
  <img
    src="hero-800.jpg"
    srcset="hero-400.jpg 400w,
            hero-800.jpg 800w,
            hero-1600.jpg 1600w"
    sizes="(max-width: 640px) 100vw, 80vw"
    width="1600"
    height="900"
    alt="Atelier fotografic"
    loading="lazy"
  >
</picture>
Pare complex dar e logic: AVIF (cel mai mic) → WebP (mediu) → JPG (fallback). Fiecare format oferă 3 dimensiuni. Browser-ul alege combinația optimă. Imaginile sunt lazy-loaded. Dimensiunile previn layout shift.
CSS minim pentru imagini
Care e regula CSS de bază pe care ar trebui să o ai pentru toate imaginile dintr-un site responsive?
Recapitulare
  • Regula globală: img { max-width: 100%; height: auto; display: block; }
  • object-fit: cover pentru imagini cu dimensiuni fixe
  • srcset + sizes pentru versiuni multiple la rezoluții diferite
  • <picture> pentru art direction (crop-uri diferite) sau formate multiple
  • WebP și AVIF sunt mai eficiente decât JPG/PNG
  • loading="lazy" pentru imagini sub fold
  • Specifică width și height pentru a preveni layout shift
Modulul 6 · Lecția 68 min citire

Container queries

Media queries răspund la dimensiunea viewport-ului. Dar dacă un card arată aceeași variantă într-un sidebar îngust și într-o zonă principală lată? Media queries nu pot distinge. Container queries pot. E cel mai important feature CSS din ultimul deceniu.

Problema cu media queries

Să zicem că ai un component „card de produs" cu imagine și text. Îl folosești în două locuri:

  • În sidebar (300px lățime) — text sub imagine, compact
  • În main content (800px lățime) — text lângă imagine, relaxat

Cu media queries, nu poți face asta ușor. La 800px viewport, sidebar-ul are tot 300px, dar media query-ul privește viewport-ul. Cardul din sidebar va arăta la fel ca cel din main.

Container queries — soluția

Container queries îți permit să aplici stiluri bazat pe dimensiunea container-ului părinte, nu a viewport-ului.

/* Pas 1: declari un container */
.card-wrapper {
  container-type: inline-size;
}

/* Pas 2: folosești container query */
.card {
  display: flex;
  flex-direction: column;
}

@container (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

Acum cardul se adaptează la lățimea container-ului său, nu la viewport. Într-un sidebar de 300px, e vertical. Într-o zonă de 800px, e orizontal. Același component, contextual.

Cele 3 pași

Pas 1: Declari container-ul

.card-wrapper {
  container-type: inline-size;
  /* Alternativ: container-type: size; (pentru ambele axe) */
}

inline-size = observă lățimea. E tot ce-ți trebuie de obicei.

Pas 2: (Opțional) Dai nume container-ului

.card-wrapper {
  container-type: inline-size;
  container-name: card;
  /* Sau combinat: */
  container: card / inline-size;
}

Pas 3: Folosești în CSS

@container (min-width: 400px) {
  /* fără nume — se aplică containerului cel mai apropiat */
  .card {
    flex-direction: row;
  }
}

@container card (min-width: 400px) {
  /* cu nume — se referă explicit la containerul „card" */
  .card {
    flex-direction: row;
  }
}

Exemplu complet — card adaptabil

.card-wrapper {
  container-type: inline-size;
}

.card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1rem;
}

.card-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

@container (min-width: 400px) {
  .card {
    flex-direction: row;
    gap: 1.5rem;
    padding: 1.5rem;
  }

  .card-image {
    width: 160px;
    height: auto;
    flex-shrink: 0;
  }
}

În orice pagină, cardul se adaptează la spațiul disponibil. Fără să știi unde e folosit.

Unități de container — cqw, cqh

Similar cu vw/vh, dar relativ la container:

  • cqw — 1% din lățimea container-ului
  • cqh — 1% din înălțimea container-ului
  • cqi — inline (de obicei lățime)
  • cqb — block (de obicei înălțime)
.card-title {
  /* scalează cu container-ul, nu cu viewport-ul */
  font-size: clamp(1rem, 4cqw, 2rem);
}
Container queries vs media queries — când care

Ambele sunt utile:

  • Media queries pentru schimbări la nivel de pagină — layout general, sidebar show/hide, navbar hamburger
  • Container queries pentru componente reutilizabile care trebuie să se adapteze la context

Container queries nu înlocuiesc media queries — le completează.

Suport browser

Container queries sunt suportate în orice browser modern din 2023. Chrome, Safari, Firefox, Edge — toate merg. Dacă proiectul tău țintește browsere foarte vechi (mai vechi de 2 ani), verifică pe caniuse.com.

Exemplu explicat
Widget reutilizabil care se adaptează
/* Widget de statistici utilizabil în orice context */
.stat-widget-wrapper {
  container-type: inline-size;
}

.stat-widget {
  padding: 1rem;
  background: white;
  border-radius: 8px;
}

.stat-value {
  font-size: 1.5rem;
  font-weight: 500;
}

.stat-label {
  font-size: 0.875rem;
  color: #666;
}

/* Pe container mai mare de 300px, mărim */
@container (min-width: 300px) {
  .stat-value {
    font-size: 2rem;
  }
  .stat-label {
    font-size: 1rem;
  }
}

/* Pe container mai mare de 500px, layout orizontal */
@container (min-width: 500px) {
  .stat-widget {
    display: flex;
    align-items: baseline;
    gap: 1rem;
  }
  .stat-value {
    font-size: 3rem;
  }
}
Același widget. În sidebar compact, arată small-stacked. În dashboard lat, arată big-horizontal. Decizia ta e bazată pe spațiul disponibil, nu pe unde e plasat.
Alege corect
Ai un component „card articol" folosit atât pe homepage (lătime mare) cât și în sidebar (lățime mică). Vrei ca în spațiu mic să arate vertical și în spațiu mare să arate orizontal. Ce folosești?
Recapitulare
  • Container queries răspund la dimensiunea container-ului părinte, nu a viewport-ului
  • Utile pentru componente reutilizabile în contexte diferite
  • Pas 1: container-type: inline-size pe părinte
  • Pas 2: @container (min-width: X) pentru reguli
  • Unități noi: cqw, cqh, cqi, cqb
  • Nu înlocuiesc media queries — se completează
  • Suport modern browser din 2023
Modulul 6 · Lecția 714 min citire

Proiect: landing responsive

Facem un landing page complet responsive — genul pe care l-ai pune pentru orice produs sau serviciu. Arată profesional pe telefon, tabletă și desktop. Mobile-first, cu tipografie fluidă și fără bug-uri.

Structura proiectului

  • Navbar cu logo și hamburger pe mobil
  • Hero cu titlu mare, subtitlu, CTA
  • Features — 3 cards
  • Call to action final
  • Footer

Pasul 1: HTML-ul

<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="styles.css">
  <title>Zest — Coaching pentru dev-eri</title>
</head>
<body>

  <header class="navbar">
    <a class="brand" href="/">Zest</a>
    <button class="menu-toggle" aria-label="Meniu">☰</button>
    <nav class="nav-links">
      <a href="#">Features</a>
      <a href="#">Pricing</a>
      <a href="#">About</a>
    </nav>
    <a class="cta-btn" href="#">Începe</a>
  </header>

  <section class="hero">
    <h1>Devino <em>dev-ul</em> pe care ai fi vrut să-l ai ca mentor.</h1>
    <p class="lead">Coaching 1-la-1 pentru dev-eri care vor să crească mai repede decât tutorialele permit.</p>
    <div class="hero-ctas">
      <a href="#" class="btn btn-primary">Programează o discuție</a>
      <a href="#" class="btn btn-outline">Vezi cursurile</a>
    </div>
  </section>

  <section class="features">
    <h2>De ce Zest</h2>
    <div class="features-grid">
      <article class="feature-card">
        <div class="feature-icon">★</div>
        <h3>Feedback personal</h3>
        <p>Trimiți codul tău, primești review în 24h. Nu ești niciodată blocat.</p>
      </article>
      <article class="feature-card">
        <div class="feature-icon">◆</div>
        <h3>Roadmap clar</h3>
        <p>De la junior la senior cu pași concreți. Nu înveți la întâmplare.</p>
      </article>
      <article class="feature-card">
        <div class="feature-icon">▲</div>
        <h3>Comunitate reală</h3>
        <p>Discord activ cu dev-eri la toate nivelurile. Întrebi, primești răspunsuri.</p>
      </article>
    </div>
  </section>

  <section class="cta">
    <h2>Pregătit să începem?</h2>
    <p>Prima discuție e gratuită. 30 minute care îți pot schimba cariera.</p>
    <a href="#" class="btn btn-primary btn-large">Rezervă acum</a>
  </section>

  <footer>
    <p>© 2026 Zest — construit cu grijă</p>
  </footer>

</body>
</html>

Pasul 2: Reset și tokens

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
body { line-height: 1.5; }
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: none; background: none; }

:root {
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-bg: #fbfaf7;
  --color-panel: #ffffff;
  --color-accent: #c8553d;
  --color-accent-hover: #a8432e;
  --color-border: rgba(0, 0, 0, 0.08);

  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;

  /* Tipografie fluidă */
  --fs-body: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fs-lead: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
  --fs-h3: clamp(1.25rem, 1.1rem + 0.7vw, 1.625rem);
  --fs-h2: clamp(2rem, 1.5rem + 2vw, 3rem);
  --fs-h1: clamp(2.5rem, 1.5rem + 4.5vw, 5.5rem);

  /* Spațiere fluidă */
  --space-section: clamp(3rem, 8vw, 7rem);
  --space-inline: clamp(1rem, 4vw, 3rem);

  --radius: 10px;
  --radius-lg: 14px;
}

body {
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  color: var(--color-text);
  background: var(--color-bg);
}

Pasul 3: Navbar responsive

.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem var(--space-inline);
  background: var(--color-panel);
  border-bottom: 1px solid var(--color-border);
}

.brand {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-style: italic;
}

.menu-toggle {
  font-size: 1.5rem;
  padding: 0.25rem;
}

.nav-links {
  display: none; /* Ascuns pe mobil */
}

.cta-btn {
  display: none; /* Ascuns pe mobil */
}

/* Tablet și peste */
@media (min-width: 768px) {
  .menu-toggle { display: none; }

  .nav-links {
    display: flex;
    gap: 2rem;
  }

  .nav-links a {
    color: var(--color-text-soft);
    font-weight: 500;
    transition: color 150ms;
  }

  .nav-links a:hover {
    color: var(--color-accent);
  }

  .cta-btn {
    display: inline-block;
    padding: 0.625rem 1.25rem;
    background: var(--color-text);
    color: var(--color-bg);
    border-radius: var(--radius);
    font-weight: 500;
    transition: background 150ms;
  }

  .cta-btn:hover {
    background: var(--color-accent);
  }
}

Pasul 4: Hero cu tipografie fluidă

.hero {
  padding: var(--space-section) var(--space-inline);
  text-align: center;
  max-width: 900px;
  margin: 0 auto;
}

.hero h1 {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: 1.05;
  letter-spacing: -0.03em;
  font-weight: 400;
  margin-bottom: 1.5rem;
}

.hero h1 em {
  font-style: italic;
  color: var(--color-accent);
}

.hero .lead {
  font-size: var(--fs-lead);
  color: var(--color-text-soft);
  max-width: 640px;
  margin: 0 auto 2.5rem;
  line-height: 1.5;
}

.hero-ctas {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

.btn {
  display: inline-block;
  padding: 0.875rem 1.75rem;
  border-radius: var(--radius);
  font-weight: 500;
  transition: background 150ms, transform 150ms;
}

.btn-primary {
  background: var(--color-accent);
  color: white;
}

.btn-primary:hover {
  background: var(--color-accent-hover);
  transform: translateY(-1px);
}

.btn-outline {
  border: 1px solid var(--color-border);
  color: var(--color-text);
}

.btn-outline:hover {
  border-color: var(--color-text);
}

.btn-large {
  padding: 1.125rem 2.25rem;
  font-size: 1.125rem;
}

Pasul 5: Features cu Grid auto-fit

.features {
  padding: var(--space-section) var(--space-inline);
  max-width: 1200px;
  margin: 0 auto;
}

.features h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 400;
  letter-spacing: -0.02em;
  text-align: center;
  margin-bottom: 3rem;
}

.features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

.feature-card {
  padding: 2rem;
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
}

.feature-icon {
  width: 48px;
  height: 48px;
  background: var(--color-accent);
  color: white;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  margin-bottom: 1rem;
}

.feature-card h3 {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 400;
  margin-bottom: 0.75rem;
}

.feature-card p {
  color: var(--color-text-soft);
  line-height: 1.6;
}

Pasul 6: CTA final și footer

.cta {
  padding: var(--space-section) var(--space-inline);
  text-align: center;
  background: var(--color-text);
  color: var(--color-bg);
}

.cta h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 400;
  letter-spacing: -0.02em;
  margin-bottom: 1rem;
}

.cta p {
  font-size: var(--fs-lead);
  opacity: 0.8;
  margin-bottom: 2.5rem;
}

footer {
  padding: 2rem var(--space-inline);
  text-align: center;
  color: var(--color-text-soft);
  font-size: 0.875rem;
  border-top: 1px solid var(--color-border);
}

Ce face landing-ul ăsta remarcabil

  • Tipografia e fluidă — clamp pe toate dimensiunile de text. Scalează continuu de la mobil la desktop mare.
  • Spațierea e fluidă--space-section și --space-inline se ajustează cu ecranul.
  • Features-grid e auto-responsiveauto-fit + minmax face treaba, fără media queries.
  • Un singur breakpoint — doar navbar-ul are media query (pentru hamburger vs full menu).
  • Butoanele se înfășoarăflex-wrap: wrap pe hero-ctas.
Ce mai trebuie adăugat

Pentru un proiect real, mai ai de făcut:

  • JavaScript pentru hamburger menu (deschide/închide)
  • Animații la scroll (deja cu prefers-reduced-motion guard)
  • Formular de înscriere funcțional
  • Analytics
  • Optimizare imagini (dacă adaugi)

Dar structura responsive e gata. HTML-ul și CSS-ul de aici pot fi baza unui site real.

Recapitulare
  • Design tokens cu variabile CSS + clamp = tipografie fluidă la scară întregului site
  • Un singur breakpoint (768px) pentru navbar — restul e deja fluid
  • Grid auto-fit face features responsive gratuit
  • Butoane cu flex-wrap = adaptare automată pe mobil
  • Spațierea fluidă cu clamp dă site-ului ritm natural pe orice ecran

🎉 Ai terminat Modulul 6!
Site-urile tale funcționează pe orice ecran. Urmează Modulul 7 (Selectori avansați) — unde CSS-ul devine cu adevărat expresiv.

Modulul 7 · Lecția 18 min citire

Combinatori

Până acum ai folosit selectori simpli — după clasă, tag sau id. Combinatorii îți permit să spui „acest element, dar numai când e într-un anumit context". E diferența între a stiliza toate link-urile și a stiliza doar link-urile dintr-un nav.

Cei patru combinatori

În CSS există 4 combinatori, fiecare cu sintaxă și scop diferite:

  • Spațiu — descendent (oricât de adânc)
  • > — copil direct
  • + — adjacent (fratele imediat)
  • ~ — general sibling (toți frații)

Spațiu — descendent

Cel mai comun. „Toate X-urile care sunt înăuntrul Y-ului, indiferent cât de adânc":

nav a {
  color: var(--color-accent);
}
/* Toate <a> dinăuntrul unui <nav> */
<nav>
  <ul>
    <li>
      <a href="#">Link</a> <!-- afectat, chiar dacă e adânc -->
    </li>
  </ul>
</nav>
<a href="#">Alt link</a> <!-- NU e afectat, e în afara nav -->

> — copil direct

Doar copiii imediați, nu descendenții mai adânci:

.menu > li {
  border-bottom: 1px solid #ddd;
}
<ul class="menu">
  <li>Acasă <!-- ✓ afectat (copil direct) -->
    <ul>
      <li>Sub-pagină</li> <!-- ✗ NU e afectat (nepot) -->
    </ul>
  </li>
</ul>

Util pentru navigare multi-nivel unde vrei să stilizezi doar elementele de top, nu și sub-meniurile.

+ — adjacent sibling

Elementul care vine imediat după un alt element (același părinte):

h2 + p {
  font-size: 1.125rem;
  color: var(--color-text-soft);
}
<h2>Titlu</h2>
<p>Primul paragraf — devine lead</p> <!-- ✓ afectat -->
<p>Al doilea paragraf</p> <!-- ✗ NU e afectat -->

Util pentru „intro paragraph" după titlu, separatoare între items, etc.

~ — general sibling

Toți frații care vin după un element, nu doar primul:

h2 ~ p {
  color: var(--color-text-soft);
}
<h2>Titlu</h2>
<p>Primul paragraf</p> <!-- ✓ -->
<p>Al doilea paragraf</p> <!-- ✓ -->
<div>Un div între</div>
<p>Al treilea paragraf</p> <!-- ✓ (încă după h2) -->
<h1>Alt titlu</h1>
<p>Ultim paragraf</p> <!-- ✓ (încă după primul h2) -->

Combinații multiple

Poți înlănțui combinatori:

.sidebar > ul li a:hover {
  /* Link-uri cu hover, în orice li, care e copil direct al ul,
     care e copil direct al .sidebar */
}

Puternic, dar folosit cu grijă. Cu cât selectorul e mai specific, cu atât e mai greu de suprascris.

Combinatori vs clase BEM

Multe proiecte moderne evită combinatorii adânci în favoarea claselor explicite (BEM, Tailwind):

/* Cu combinatori — elegant dar fragil */
.card .header .title {
  font-size: 1.5rem;
}

/* Cu clase — verbose dar robust */
.card-title {
  font-size: 1.5rem;
}

Pentru componente mari, clase explicite sunt mai lizibile. Pentru situații punctuale (primul paragraf după h2, link-uri în nav), combinatori sunt ideali.

Exemplu explicat
Blog post cu tipografie contextuală
article {
  max-width: 65ch;
}

/* Primul paragraf după fiecare h2 e lead */
article h2 + p {
  font-size: 1.25rem;
  color: var(--color-text-soft);
}

/* Paragraf imediat după listă are margin extra */
article ul + p,
article ol + p {
  margin-top: 2rem;
}

/* Doar link-urile din articol, nu cele din nav/footer */
article a {
  color: var(--color-accent);
  text-decoration: underline;
}
Combinatorii îți dau control fin asupra tipografiei fără să adaugi clase peste tot. Blog post-urile arată tipărit, nu generic.
Ce selectează
Ce selectează .card > h2?
Recapitulare
  • Spațiu = descendent (A B)
  • > = copil direct (A > B)
  • + = frate imediat (A + B)
  • ~ = toți frații după (A ~ B)
  • Pentru componente mari, preferă clase explicite
  • Pentru tipografie contextuală, combinatorii sunt ideali
Modulul 7 · Lecția 210 min citire

Pseudo-classes

Pseudo-classes îți permit să stilizezi elemente bazat pe stare sau poziție — lucruri care nu sunt reflectate direct în HTML. Hover, focus, primul copil, al n-lea element. Fără ele, interactivitatea CSS-ului ar fi moartă.

Stări interactive — :hover, :focus, :active

:hover — mouse peste element

.button {
  background: var(--color-accent);
  transition: background 150ms;
}

.button:hover {
  background: var(--color-accent-hover);
}

:focus și :focus-visible

:focus = când elementul e focusat (tab sau click). :focus-visible = doar când focus-ul e făcut prin tastatură.

/* Outline doar pentru utilizatorii cu tastatură */
button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

button:focus {
  outline: none; /* scoatem outline-ul default urât */
}

Important pentru accesibilitate: nu scoate toate outline-urile. Utilizatorii care navighează cu tastatură au nevoie de ele.

:active — în timpul click-ului

.button:active {
  transform: scale(0.98); /* efect de „apăsat" */
}

Stări de formular — :checked, :disabled, :required

input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

input:required {
  border-left: 3px solid var(--color-accent);
}

input:invalid {
  border-color: red;
}

input:valid {
  border-color: green;
}

/* Când checkbox e bifat */
input[type="checkbox"]:checked + label {
  font-weight: 600;
}

Pseudo-classes de poziție — :first-child, :last-child, :nth-child

Primul și ultimul

li:first-child {
  border-top: none;
}

li:last-child {
  border-bottom: none;
}

:nth-child — poziții specifice

/* Al 3-lea copil */
li:nth-child(3) { color: red; }

/* Toți copiii pari */
li:nth-child(even) { background: #f5f5f5; }

/* Toți copiii impari */
li:nth-child(odd) { background: white; }

/* Formula an+b */
li:nth-child(3n) { /* al 3-lea, al 6-lea, al 9-lea... */ }

/* Primii 3 */
li:nth-child(-n+3) { /* 1, 2, 3 */ }

/* De la al 4-lea încolo */
li:nth-child(n+4) { }

:nth-of-type — similar dar pentru tag

Diferența subtilă dar importantă:

  • p:nth-child(2) = „al doilea copil, dacă e p"
  • p:nth-of-type(2) = „al doilea p din părinte"
<div>
  <h2>Titlu</h2>    <!-- copil 1, tip h2 nr 1 -->
  <p>P1</p>          <!-- copil 2, tip p nr 1 -->
  <p>P2</p>          <!-- copil 3, tip p nr 2 -->
</div>

/* p:nth-child(2) = P1 (al doilea copil, care e p) */
/* p:nth-of-type(2) = P2 (al doilea p) */

Alte pseudo-classes utile

  • :empty — elementul nu are copii/conținut
  • :not(selector) — exclude elemente
  • :is(s1, s2, s3) — unul din selectori (mai scurt decât a scrie toate)
  • :where(...) — ca :is dar fără impact pe specificitate
/* Toate butoanele care NU au clasa „primary" */
button:not(.primary) {
  background: transparent;
  border: 1px solid #ddd;
}

/* Headers mai concis */
:is(h1, h2, h3) {
  font-family: var(--font-display);
}
/* Echivalent cu: h1, h2, h3 { ... } dar mai ușor de extins */
Transition mereu cu :hover

Schimbările bruște între stări sunt urâte. Adaugă mereu transition:

.button {
  background: var(--color-accent);
  transition: background 150ms ease, transform 150ms ease;
}

.button:hover {
  background: var(--color-accent-hover);
}

.button:active {
  transform: scale(0.98);
}

150-250ms e intervalul optim pentru majoritatea tranziţiilor. Sub 100ms pare instant. Peste 400ms pare lent.

Exemplu explicat
Tabel cu rânduri alternante și hover
table { width: 100%; }

tbody tr:nth-child(even) {
  background: #fafafa;
}

tbody tr:hover {
  background: #f0e8e3;
}

tbody tr:first-child td {
  border-top: 2px solid var(--color-text);
}

tbody tr:last-child td {
  border-bottom: 2px solid var(--color-text);
}
Rânduri alternante pentru lizibilitate, hover pentru interacțiune, borduri mai groase la extremități pentru separare vizuală. Totul fără să adaugi clase pe fiecare rând.
Aplică corect
Vrei să stilizezi ultimul link dintr-un nav diferit (fără border-right). Care e cel mai curat mod?
Recapitulare
  • Stări: :hover, :focus, :focus-visible, :active
  • Formular: :checked, :disabled, :required, :invalid, :valid
  • Poziție: :first-child, :last-child, :nth-child(n)
  • :not(), :is(), :where() pentru combinații
  • Adaugă mereu transition pentru schimbări fluide (150-250ms)
  • Nu scoate outline fără să pui alt indicator de focus
Modulul 7 · Lecția 39 min citire

Pseudo-elements

Pseudo-elements îți permit să stilizezi părți ale elementelor care nu există în HTML — prima literă, prima linie, conținut înainte sau după. Cu ele adaugi decorațiuni, iconițe și detalii fără să aglomerezi HTML-ul.

Două colon-uri vs unul

Pseudo-classes folosesc un colon (:hover). Pseudo-elements folosesc două (::before). Diferența vizibilă.

Moștenire istorică: în CSS 2, se folosea un colon pentru ambele. CSS 3 a introdus :: pentru pseudo-elements. Sintaxa veche încă funcționează dar folosește :: pentru claritate.

::before și ::after — conținut generat

Cele mai folosite pseudo-elements. Adaugă conținut înainte sau după, fără să atingi HTML-ul:

.quote::before {
  content: "";
}

.quote::after {
  content: "";
}

content e obligatoriu — fără el, pseudo-element-ul nu apare. Poate fi text, gol ("") pentru forme geometrice, sau attr() pentru a lua valori din atribute.

Forme și decorațiuni cu ::before/::after

/* Separator între secțiuni */
section + section::before {
  content: "";
  display: block;
  width: 40px;
  height: 2px;
  background: var(--color-accent);
  margin: 2rem auto;
}

/* Iconiță înainte de link */
.external-link::after {
  content: " ↗";
  font-size: 0.85em;
}

/* Bullet personalizat pe liste */
ul.custom li::before {
  content: "→ ";
  color: var(--color-accent);
  font-weight: 700;
}

/* Înlocuiește default-ul */
ul.custom {
  list-style: none;
  padding-left: 1.5rem;
}

Folosire pentru detalii vizuale

.card {
  position: relative;
  padding: 2rem;
}

/* Bară decorativă în colțul stâng */
.card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  background: var(--color-accent);
}

attr() — conținut dinamic

Poți lua conținutul dintr-un atribut HTML:

<a href="mailto:hello@example.com" data-tooltip="Trimite email">
  ✉
</a>
a[data-tooltip]:hover::after {
  content: attr(data-tooltip);
  position: absolute;
  background: black;
  color: white;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 0.85rem;
}

Tooltip pur-CSS, fără JavaScript. attr(data-tooltip) extrage valoarea atributului și o afișează.

::first-letter și ::first-line

Pentru efecte tipografice clasice:

/* Drop cap ca în carte */
article p:first-child::first-letter {
  font-family: var(--font-display);
  font-size: 4rem;
  float: left;
  line-height: 0.9;
  margin-right: 0.5rem;
  color: var(--color-accent);
}

/* Prima linie în small-caps */
p::first-line {
  font-variant: small-caps;
  color: var(--color-text-soft);
}

::selection — text selectat

::selection {
  background: var(--color-accent);
  color: white;
}

Când utilizatorul selectează text pe pagină, apare cu culoarea ta de brand în loc de albastrul default. Detaliu subtil, dă pagini polish.

::placeholder — text placeholder din input-uri

input::placeholder {
  color: var(--color-text-soft);
  font-style: italic;
}

::marker — bullet-urile din liste

ul li::marker {
  color: var(--color-accent);
  font-weight: 700;
}

ol li::marker {
  font-family: var(--font-display);
}
content gol vs fără content

Dacă omiți content, pseudo-element-ul nu apare deloc:

/* Nu apare nimic */
.box::before {
  background: red;
  width: 10px;
  height: 10px;
}

/* Apare pătratul roșu */
.box::before {
  content: "";  /* obligatoriu! */
  background: red;
  width: 10px;
  height: 10px;
  display: block;
}

content: "" (string gol) e pentru pseudo-elements pur decorative — un pătrat, o linie, o formă geometrică.

Limitări ale pseudo-elements

  • Nu funcționează pe elemente self-closing (<img>, <input>, <br>)
  • Nu sunt accesibile pentru cititoare de ecran (info vizuală, nu semantică)
  • Un singur ::before și un singur ::after per element
Exemplu explicat
Card cu accent vizual și badge
<article class="product-card new">
  <h3>Produs</h3>
  <p>Descriere</p>
</article>
.product-card {
  position: relative;
  padding: 2rem;
  background: white;
  border-radius: 12px;
}

/* Linie de accent în colț */
.product-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 60px;
  height: 4px;
  background: var(--color-accent);
  border-radius: 0 0 4px 0;
}

/* Badge „NEW" în colțul dreapta */
.product-card.new::after {
  content: "NEW";
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: var(--color-accent);
  color: white;
  padding: 4px 10px;
  border-radius: 100px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}
Două decorațiuni vizuale fără niciun HTML extra. Accentul linear e pe fiecare card. Badge-ul „NEW" apare doar pe cele cu clasa .new.
Pseudo-element corect
Vrei să adaugi o săgeată „→" automat după toate link-urile cu clasa .external. Ce sintaxă folosești?
Recapitulare
  • :: pentru pseudo-elements (nu : care e pentru pseudo-classes)
  • content e obligatoriu pe ::before și ::after
  • content: "" pentru decorațiuni, content: "text" pentru text
  • attr(nume) extrage valoare din atribute HTML
  • Alte pseudo-elements: ::first-letter, ::selection, ::placeholder, ::marker
  • Nu funcționează pe <img>, <input>, <br>
Modulul 7 · Lecția 48 min citire

:has() — selectorul părinte

Pentru 20 de ani, CSS n-a avut selector părinte. „Stilizează un card dacă conține o imagine" — imposibil în CSS. Trebuia JavaScript. În 2023, :has() a schimbat asta. E probabil cel mai important feature CSS din ultimii 10 ani.

Problema

Să zicem că ai carduri. Unele au imagine, altele nu. Vrei cardurile cu imagine să aibă un layout diferit — imaginea sus, textul jos.

Înainte de :has(), soluții:

  • Adăugai o clasă specială (.card-with-image) manual — muncă și fragil
  • Foloseai JavaScript să detectezi și adaugi clasa — complexitate inutilă
  • Aveai două componente CSS diferite — duplicare

Soluția modernă

.card:has(img) {
  display: flex;
  flex-direction: column;
}

.card:has(img) img {
  order: -1; /* imaginea sus */
}

Traducere: „Cardurile care conțin o imagine". CSS pur, zero HTML modificări, zero JavaScript.

Sintaxa

parinte:has(copil) {
  /* stilizezi părintele pe baza a ceea ce conține */
}

Exemple practice

Card cu badge

/* Card cu badge „NEW" primește margin extra sus */
.product:has(.badge-new) {
  margin-top: 2rem;
}

Form cu erori

/* Form-ul devine roșu dacă are erori */
form:has(input:invalid) {
  border-color: red;
}

/* Buton submit dezactivat vizual dacă form-ul nu e valid */
form:has(input:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

Section cu titlu

/* Section care conține h2 primește padding mai mare */
section:has(h2) {
  padding-top: 4rem;
}

/* Section fără titlu rămâne cu padding normal */
section:not(:has(h2)) {
  padding-top: 2rem;
}

Nav cu sub-meniu

/* Item de nav care are sub-meniu primește săgeată */
nav li:has(ul)::after {
  content: " ▼";
  font-size: 0.7em;
}

Combinare cu alte selectori

:has() poate fi combinat cu orice:

/* Card care conține O imagine ȘI un tag de preț */
.product:has(img):has(.price) {
  border: 2px solid var(--color-accent);
}

/* Card care NU conține imagine */
.product:not(:has(img)) {
  background: #f5f5f5;
}

/* Article care are h2 + p imediat după */
article:has(h2 + p) {
  /* stilizare specifică */
}

Selectoare complexe înăuntrul :has()

/* Form care conține un input cu eroare */
form:has(input.error) {
  border-color: red;
}

/* Card care conține cel puțin 3 imagini */
.gallery:has(img:nth-of-type(3)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Body care conține un modal activ */
body:has(.modal.active) {
  overflow: hidden; /* oprește scroll-ul paginii */
}
Un exemplu real — dark mode automat

Poți face dark mode care se activează când un checkbox e bifat, fără JavaScript:

<input type="checkbox" id="dark-mode">
<label for="dark-mode">Dark mode</label>
<main>...</main>
body:has(#dark-mode:checked) {
  --color-bg: #1a1814;
  --color-text: #e8e3d5;
}

Când checkbox-ul e bifat, :has() schimbă variabilele CSS pe body. Toată pagina devine dark mode. Fără JavaScript, fără reload.

Suport browser

:has() e suportat în toate browserele moderne din 2023. Chrome 105+, Safari 15.4+, Firefox 121+, Edge modern. Dacă proiectul tău nu trebuie să suporte Firefox vechi, poți folosi :has().

Exemplu explicat
Layout dinamic de grid bazat pe conținut
.gallery {
  display: grid;
  gap: 1rem;
}

/* Dacă are 1 item, un singur card mare */
.gallery:has(> :only-child) {
  grid-template-columns: 1fr;
}

/* Dacă are 2 items, split în 2 */
.gallery:has(> :nth-child(2):last-child) {
  grid-template-columns: 1fr 1fr;
}

/* Dacă are 3 items, 2+1 */
.gallery:has(> :nth-child(3):last-child) {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
}
.gallery:has(> :nth-child(3):last-child) > :first-child {
  grid-column: 1 / -1;
}

/* 4+ items — grid responsive normal */
.gallery:has(> :nth-child(4)) {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
Galeria își schimbă layout-ul bazat pe câte items are. Imposibil fără :has() în CSS pur înainte de 2023.
Scenariu
Vrei să marchezi vizual (background galben) orice card care conține un label „Urgent". Cum faci?
Recapitulare
  • :has() = selectorul părinte, absent din CSS 20 de ani
  • Sintaxă: părinte:has(copil) { ... }
  • Funcționează cu orice selectori înăuntru (clase, stări, combinatori)
  • Cazuri de utilizare: layout contextual, form validation, dark mode
  • Suport modern browser din 2023 încoace
  • Înlocuiește foarte multă JavaScript condițional
Modulul 7 · Lecția 59 min citire

Specificitate și @layer

„De ce nu se aplică CSS-ul meu?!" Cea mai comună întrebare a începătorilor. Răspunsul aproape mereu: specificitate. În lecția asta demistificăm regulile și învățăm @layer, soluția modernă pentru probleme de specificitate.

Ce e specificitatea

Când mai multe selectori targetează același element, CSS trebuie să decidă care câștigă. Regula: selectorul mai specific câștigă. Dacă sunt la fel de specifici, cel care apare mai târziu în CSS câștigă.

Sistemul de puncte

Specificitatea se calculează pe 4 coloane (0-0-0-0):

  • Inline styles (style="...") = 1-0-0-0
  • ID-uri (#header) = 0-1-0-0
  • Clase, atribute, pseudo-classes = 0-0-1-0
  • Elemente, pseudo-elements = 0-0-0-1

Exemple de calcul

/* 0-0-0-1 (un element) */
p { color: red; }

/* 0-0-1-0 (o clasă) */
.text { color: blue; }

/* 0-0-1-1 (o clasă + un element) */
p.text { color: green; }

/* 0-0-2-1 (două clase + un element) */
.article p.text { color: purple; }

/* 0-1-0-0 (un id) */
#main { color: orange; }

/* 0-1-1-1 (id + clasă + element) */
#main p.text { color: black; }

Cu cât mai sus în ierarhie, cu atât mai puternic. Un ID bate orice număr de clase.

De ce să eviți IDs pentru stilizare

IDs au specificitate atât de mare încât greu le suprascrii:

#header .link { color: red; }
/* 0-1-1-0 = puternic */

.header .link.special { color: blue; }
/* 0-0-3-0 = slab, NU bate id-ul */

Chiar și cu 3 clase, nu suprascrii un singur ID. De aceea folosim clase pentru stilizare și IDs doar pentru JavaScript/anchor links.

!important — arma nucleară

!important sare peste tot sistemul de specificitate:

.text {
  color: red !important;
  /* bate orice selector mai specific */
}

Evită !important. Folosit odată, devine epidemie — trebuie !important să bată alte !important. Ajungi cu CSS imposibil de întreținut.

Excepții acceptabile pentru !important:

  • Override la stiluri third-party pe care nu le poți schimba
  • Utility classes (ex. .hidden { display: none !important; })
  • Accesibilitate critică (focus indicator care nu trebuie ascuns)

@layer — soluția modernă

Din 2022, CSS are @layer — o cale de a controla specificitatea fără hack-uri. Definești straturi în ordinea dorită, iar regulile din straturile ulterioare bat pe cele din straturile anterioare, indiferent de specificitatea selectoarelor.

@layer reset, base, components, utilities;

@layer reset {
  * { box-sizing: border-box; }
}

@layer base {
  body { font-family: sans-serif; }
  h1 { font-size: 2rem; }
}

@layer components {
  .card { padding: 2rem; }
  #special-card { background: yellow; }
}

@layer utilities {
  .text-red { color: red; }
}

În exemplul de mai sus:

  • .text-red din utilities bate #special-card din components
  • Chiar dacă ID-ul are specificitate mai mare, layer-ul de după câștigă

De ce e util @layer

Îți permite să organizezi CSS-ul pe straturi și să controlezi prioritatea explicit:

  • reset — normalize.css sau reset-ul tău
  • base — tipografie, body styles
  • components — card, button, navbar
  • utilities — helper classes (margin, padding, display)

Utilities au prioritate explicită peste componente. Componente peste base. Nu mai trebuie să lupți cu specificitatea selectoarelor.

Importuri în layer

@import url('reset.css') layer(reset);
@import url('tailwind.css') layer(utilities);

Poți importa librării direct într-un layer, controlând de la cine vin stilurile.

Reguli fără layer

CSS-ul care NU e în nicio @layer are cea mai mare prioritate — bate toate layer-urile:

@layer components {
  .button { background: blue; }
}

/* Fără layer — bate layer-ul „components" */
.button { background: red; }

Asta te poate încurca la început. Regula: ori pui totul în layers, ori nimic. Consistent e mai bine.

Cum rezolvi probleme de specificitate

  1. Deschide DevTools → selectează elementul → uită-te la „Computed" sau „Styles"
  2. Vezi care regulă câștigă și care sunt suprascrise
  3. DevTools afișează specificitatea fiecărui selector
  4. Dacă ai nevoie de mai mare — adaugă o clasă, nu un ID
  5. Dacă ai prea multă — simplifică selectorul (ex. de la .card ul li a la .card-link)
Exemplu explicat
Structura recomandată cu @layer
@layer reset, base, layout, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  * { margin: 0; }
}

@layer base {
  body {
    font-family: var(--font-sans);
    color: var(--color-text);
  }
  h1, h2, h3 { font-family: var(--font-display); }
}

@layer layout {
  .container { max-width: 1200px; margin: 0 auto; }
  .grid { display: grid; gap: 1rem; }
}

@layer components {
  .button {
    padding: 0.75rem 1.5rem;
    background: var(--color-accent);
    color: white;
    border-radius: 8px;
  }

  .card {
    padding: 2rem;
    background: white;
    border-radius: 12px;
  }
}

@layer utilities {
  .hidden { display: none; }
  .text-center { text-align: center; }
  .mt-4 { margin-top: 1rem; }
}
Fiecare categorie are rolul ei. Utilities bat componente. Componente bat base. Zero lupte cu specificitatea — ierarhia e explicită.
Rezolvă problema
Ai .card { color: red; } și #special { color: blue; }. Care câștigă pe un element cu ambele?
Recapitulare
  • Specificitate: inline > id > class > element
  • Calculează pe 4 coloane: 0-0-0-0
  • Evită ID-uri pentru stilizare, preferă clase
  • Evită !important cu excepții rare (utilities, accesibilitate)
  • @layer controlează prioritatea prin straturi — soluția modernă
  • Ordinea layer-urilor contează: @layer reset, base, components, utilities;
Modulul 7 · Lecția 69 min citire

Atribute și selectori complecși

Selectorii de atribute îți permit să targetezi elemente bazat pe ce atribute au — href, type, data-*. Combinate cu tot ce ai învățat, construiești selectori care fac magie fără JavaScript.

Atribute de bază

/* Element cu atribut X (indiferent de valoare) */
a[target] { ... }
input[required] { ... }

/* Element cu atribut = valoare exactă */
a[target="_blank"] { ... }
input[type="email"] { ... }
a[href="https://example.com"] { ... }

Tipuri de input în CSS

Selectorii de atribute fac posibilă stilizarea distinctă pentru tipuri de input:

input[type="text"],
input[type="email"],
input[type="password"] {
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 6px;
}

input[type="checkbox"],
input[type="radio"] {
  width: 18px;
  height: 18px;
}

input[type="file"] {
  border: 2px dashed #ddd;
  padding: 1rem;
  border-radius: 8px;
}

Operatori pe atribute

^= începe cu

/* Link-uri externe (https://) */
a[href^="https://"] {
  color: var(--color-accent);
}

a[href^="https://"]::after {
  content: " ↗";
}

/* Link-uri email */
a[href^="mailto:"]::before {
  content: "✉ ";
}

/* Link-uri telefon */
a[href^="tel:"]::before {
  content: "☎ ";
}

$= se termină cu

/* Link-uri către PDF-uri */
a[href$=".pdf"]::after {
  content: " (PDF)";
  font-size: 0.85em;
  color: var(--color-text-soft);
}

/* Imagini în format webp */
img[src$=".webp"] {
  /* posibile ajustări */
}

*= conține

/* Link-uri care conțin „youtube" oriunde */
a[href*="youtube"]::before {
  content: "📺 ";
}

/* Imagini cu numele „logo" */
img[src*="logo"] {
  max-height: 40px;
}

~= cuvânt exact

Pentru liste de cuvinte separate prin spațiu:

<div data-tags="javascript html css">...</div>

/* Caută exact „html" ca cuvânt în listă */
[data-tags~="html"] {
  /* match */
}

|= începe cu urmat de dash

Specific pentru coduri de limbă:

[lang|="en"] { /* potrivește „en", „en-US", „en-GB" */ }
[lang|="ro"] { /* potrivește „ro", „ro-RO" */ }

Combinare cu pseudo-classes

/* Input text necompletat (required și empty) */
input[type="text"]:invalid {
  border-color: red;
}

/* Link extern într-un articol */
article a[href^="https://"]:hover {
  background: rgba(200, 85, 61, 0.1);
}

/* Butoane disabled cu anumit tip */
button[type="submit"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Atribute data-*

Atributele custom data-* sunt excelente pentru stilizare condițională:

<div class="status" data-state="active">Online</div>
<div class="status" data-state="away">Plecat</div>
<div class="status" data-state="offline">Offline</div>
.status { padding: 4px 12px; border-radius: 100px; }

[data-state="active"] {
  background: #e3efe5;
  color: #2d4d37;
}

[data-state="away"] {
  background: #fff4e0;
  color: #8b5e00;
}

[data-state="offline"] {
  background: #f0f0f0;
  color: #666;
}

Pattern-ul e folosit peste tot în aplicații moderne. JavaScript setează data-state, CSS reacționează.

Tooltips pur-CSS

<button data-tooltip="Salvează modificările">Salvează</button>
[data-tooltip] {
  position: relative;
}

[data-tooltip]:hover::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-text);
  color: var(--color-bg);
  padding: 4px 10px;
  border-radius: 4px;
  font-size: 0.8125rem;
  white-space: nowrap;
  pointer-events: none;
}
Specificitate la atribute

Selectorii de atribute au aceeași specificitate ca clasele (0-0-1-0):

.primary { background: blue; }     /* 0-0-1-0 */
[type="submit"] { background: red; } /* 0-0-1-0 */

/* Dacă un buton are ambele, cel mai târziu în CSS câștigă */
Exemplu explicat
Liste de link-uri stilizate contextual
nav a {
  color: var(--color-text);
  padding: 0.5rem 1rem;
}

/* Link-ul curent */
nav a[aria-current="page"] {
  background: var(--color-accent);
  color: white;
}

/* Link-uri externe */
nav a[href^="https://"] {
  color: var(--color-accent);
}

nav a[href^="https://"]::after {
  content: " ↗";
  font-size: 0.85em;
  opacity: 0.7;
}

/* Link-uri către PDF-uri */
a[href$=".pdf"] {
  border-bottom: 1px dotted;
}

a[href$=".pdf"]::after {
  content: " (PDF, " attr(data-size) ")";
  font-size: 0.85em;
  color: var(--color-text-soft);
}
Fiecare tip de link are aspect distinct, identificat doar după atribute. Zero clase adăugate manual pe link-uri. Infrastructura HTML semantică (aria-current, href) e suficientă.
Selectează corect
Vrei să stilizezi toate link-urile care trimit la fișiere ZIP. Cum faci?
Recapitulare
  • Selectori de atribute: [attr], [attr="val"], [attr^="x"], [attr$="x"], [attr*="x"]
  • Tipurile de input se selectează cu input[type="..."]
  • Link-uri externe cu a[href^="https://"]
  • Fișiere cu a[href$=".pdf"]
  • Atributele data-* sunt ideale pentru state-uri custom
  • Tooltips pur-CSS cu attr() și atribute data-*

🎉 Ai terminat Modulul 7!
Ai acum toate uneltele de selectat din CSS modern. Urmează Modulul 8 — Animații și tranziții, unde pagina prinde viață.

Modulul 8 · Lecția 19 min citire

Transitions — fundamente

Un buton care își schimbă culoarea brusc la hover pare stricat. Același buton cu o tranziție de 200ms pare profesional. Diferența: trei cuvinte de CSS. transition e cel mai ieftin „efect wow" pe care-l poți adăuga unui site.

Ce face transition

Spune browser-ului: „când o proprietate se schimbă, nu face saltul instant — fă o animație lină între vechea și noua valoare".

.button {
  background: blue;
  transition: background 200ms;
}

.button:hover {
  background: red;
}

Fără transition: salt brusc de la albastru la roșu. Cu transition: trecere lină de 200ms. Același CSS, impresie complet diferită.

Sintaxa completă

transition: [proprietate] [durata] [timing] [întârziere];

/* Exemplu complet */
.button {
  transition: background 200ms ease-in-out 50ms;
}
  • proprietate — ce se animează (background, transform, all)
  • durata — cât durează (200ms, 0.3s)
  • timing — curba (linear, ease, ease-in-out...)
  • întârziere — pauză înainte să înceapă (opțional)

Durate comune

Convenții pe care le urmează majoritatea interfețelor moderne:

  • 100-150ms — hover pe butoane, schimbări subtile
  • 200-250ms — opacity, background, culori — „sweet spot"
  • 300-400ms — transform-uri (translate, scale), panels
  • 500ms+ — tranziții mari, modale care intră/ies

Sub 100ms pare instant (fără efect). Peste 500ms pare lent și greu. Între 150-350ms e zona în care majoritatea site-urilor moderne joacă.

Timing functions — curbele mișcării

linear

Viteza constantă. Rar folosit pentru UI — pare robotic.

ease (implicit)

Pornește încet, accelerează, frânează. Natural pentru majoritatea animațiilor.

ease-in

Pornește încet, accelerează. Util când ceva „iese" din scenă.

ease-out

Pornește repede, frânează. Util când ceva „intră" în scenă. Cel mai folosit pentru UI.

ease-in-out

Încet la ambele capete. Dramatic, bun pentru animații mari.

cubic-bezier() — curbe custom

.button {
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  /* un „bounce" subtil la final */
}

Tranziții pe mai multe proprietăți

.card {
  transition:
    transform 200ms ease-out,
    box-shadow 300ms ease-out,
    background 150ms;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0,0,0,0.1);
  background: var(--color-panel);
}

Fiecare proprietate cu propria durată și curbă. Transform-ul se mișcă cu 200ms, umbra cu 300ms — mișcări care se suprapun dau impresia de profunzime.

transition: all — evitat

/* Problematic */
.button {
  transition: all 300ms;
}

transition: all animează orice schimbare — chiar și cele pe care nu le vrei animate. Performanță proastă, comportament imprevizibil. Folosește mereu proprietăți explicite.

Ce poate fi animat

Nu toate proprietățile se pot anima. Regulă: proprietăți care au valori continue (numere, culori) se animează. Proprietăți cu valori discrete (display, position) nu.

Se animează:

  • Culori (color, background, border-color)
  • Dimensiuni (width, height, padding, margin)
  • Opacitate (opacity)
  • Transform-uri (translate, rotate, scale)
  • Border, shadow, filter

Nu se animează direct:

  • display — dar poți combina cu opacity + transition-delay
  • Schimbări de tip (de la flex la block)
Performanță: transform și opacity

Nu toate animațiile sunt egale. Browser-ul poate accelera transform și opacity pe GPU — mișcări fluide chiar pe mobile lent.

Alte proprietăți (width, height, margin) cer recalculare layout — mai lent, mai sacadat.

Pentru animații fluide, folosește transform și opacity când e posibil.

Exemplu explicat
Buton modern cu multiple tranziții
.btn {
  padding: 0.75rem 1.5rem;
  background: var(--color-accent);
  color: white;
  border-radius: 8px;
  border: none;
  cursor: pointer;

  transition:
    background 150ms ease-out,
    transform 150ms ease-out,
    box-shadow 200ms ease-out;
}

.btn:hover {
  background: var(--color-accent-hover);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(200, 85, 61, 0.3);
}

.btn:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(200, 85, 61, 0.2);
  transition: all 50ms;
}
Hover: se ridică subtil, umbră crește. Active (click): revine la bază instant (tranziție rapidă de 50ms). Sensația de buton fizic, nu pixel pe ecran.
Alege durata
Care e durata optimă pentru tranziția de culoare la hover pe un buton?
Recapitulare
  • transition: proprietate durată timing-function delay
  • Durate: 150-200ms pentru hover, 300-400ms pentru transform-uri
  • ease-out e cel mai folosit timing pentru UI
  • Animează mai multe proprietăți separat, nu cu all
  • Transform și opacity sunt cele mai performante
  • Durate diferite pe proprietăți diferite = sensație de profunzime
Modulul 8 · Lecția 29 min citire

Transform

transform e o proprietate magică. Mută, rotește, mărește sau deformează elemente fără să afecteze layout-ul din jur. Accelerat de GPU, fluid pe orice device. Dacă transition e motorul animațiilor, transform e materia lor primă.

Cele 4 funcții principale

translate() — mutare

.box {
  transform: translateX(20px);    /* la dreapta cu 20px */
  transform: translateY(-10px);   /* sus cu 10px */
  transform: translate(20px, -10px); /* ambele */
}

Spre deosebire de margin sau position, translate nu afectează alte elemente. Elementul „planează" peste pagină în poziția translate-ată. Perfect pentru hover effects, drag-and-drop, etc.

scale() — mărire/micșorare

.box {
  transform: scale(1.1);     /* cu 10% mai mare */
  transform: scale(0.9);     /* cu 10% mai mic */
  transform: scale(1.5, 2);  /* mai lat 1.5x, mai înalt 2x */
}

Valoarea 1 = dimensiune originală. 2 = dublu. 0.5 = jumătate. Util pentru hover pe cards, micro-interacțiuni.

rotate() — rotație

.box {
  transform: rotate(45deg);
  transform: rotate(-90deg);
  transform: rotate(0.25turn); /* un sfert de rotație */
}

skew() — deformare

.box {
  transform: skew(20deg);     /* înclinare pe X */
  transform: skewY(10deg);    /* înclinare pe Y */
}

Folosit rar — de obicei pentru efecte vizuale decorative.

Combinarea transform-urilor

Poți combina mai multe transform-uri într-o singură declarație:

.box {
  transform: translate(20px, -10px) rotate(45deg) scale(1.2);
}

Ordinea contează. Translate apoi rotate e diferit de rotate apoi translate. Experimentează să vezi diferența.

transform-origin — punctul de pivotare

Implicit, transformele se fac în jurul centrului elementului. Poți schimba asta:

.box {
  transform-origin: top left;   /* pivot în colț stânga-sus */
  transform: rotate(45deg);
  /* acum rotația e din colțul stâng-sus, nu din centru */
}

Valori: top, bottom, left, right, center, procente, sau coordonate.

Tranziție + Transform = UI modern

Combinate, construiesc efectele pe care le vezi pe site-urile moderne:

/* Card care „se ridică" la hover */
.card {
  transition: transform 200ms ease-out;
}

.card:hover {
  transform: translateY(-6px);
}

/* Buton care se mărește subtil */
.btn {
  transition: transform 150ms ease-out;
}

.btn:hover {
  transform: scale(1.05);
}

/* Icon care se rotește la hover */
.icon {
  transition: transform 300ms ease-out;
}

.icon:hover {
  transform: rotate(180deg);
}
Transform vs poziție reală

Un aspect important: transform mută vizual elementul, dar pentru browser, elementul e în aceeași poziție ca înainte.

  • Hit detection (click) e tot în poziția vizuală (modernă)
  • Alte elemente nu observă mutarea (nu se rearanjează)
  • Perfect pentru animații — nimic nu sare pe pagină

Asta îl face diferit de position: relative; top: X care afectează layout-ul în timpul animației.

Transform-uri 3D

Pentru efecte mai dramatice, CSS suportă și 3D:

.card {
  perspective: 1000px; /* setează perspective pe părinte */
}

.card-inner {
  transform: rotateY(180deg); /* rotație 3D */
  transform-style: preserve-3d;
}

/* Efecte 3D comune */
.flip:hover {
  transform: rotateY(180deg);
}

.tilt:hover {
  transform: perspective(500px) rotateX(10deg) rotateY(-10deg);
}
Exemplu explicat
Card interactiv profesional
.product-card {
  padding: 2rem;
  background: white;
  border-radius: 12px;
  cursor: pointer;

  transition:
    transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 300ms ease-out;

  /* Box-shadow inițială subtilă */
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.product-card:hover {
  /* Se ridică și se înclină subtil */
  transform: translateY(-6px) rotate(-0.5deg);

  /* Umbra se extinde și se întunecă */
  box-shadow:
    0 12px 24px rgba(0, 0, 0, 0.1),
    0 2px 4px rgba(0, 0, 0, 0.05);
}

.product-card:active {
  /* Se „apasă" când e click */
  transform: translateY(-2px) scale(0.99);
  transition: transform 100ms;
}
Card care se ridică, se înclină ușor (-0.5deg dă senzație de „naturalitate"), umbra se extinde. Click-ul îl „apasă" înapoi. Detalii mici, impresie profesională.
Transform corect
Vrei ca un card să se ridice cu 4px la hover, fără să afecteze layout-ul. Ce folosești?
Recapitulare
  • translateX/Y — mutare fără a afecta layout-ul
  • scale() — mărire/micșorare (1 = original)
  • rotate() — rotație în grade
  • Combinare: transform: translate() rotate() scale()
  • transform-origin — schimbă punctul de pivotare
  • Transform e accelerat GPU — animații fluide
  • Nu afectează layout-ul din jur
Modulul 8 · Lecția 310 min citire

Keyframes

Transitions animează de la A la B. Keyframes îți permit să treci prin mai multe puncte: A → B → C → D. Animații complexe, loop-uri, secvențe — toate cu keyframes. E cel mai puternic nivel de animație din CSS.

Sintaxa de bază

Definești o animație cu @keyframes, apoi o aplici cu animation:

/* Definirea animației */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Aplicarea */
.element {
  animation: fadeIn 500ms ease-out;
}

from = 0%, to = 100%. Pentru animații cu mai multe etape, folosești procente.

Keyframes cu mai multe etape

@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.1);
    opacity: 0.8;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.badge {
  animation: pulse 2s ease-in-out infinite;
}

Elementul se mărește la 50% din animație, apoi revine. Cu infinite, loop continuu.

Proprietatea animation

.element {
  animation: [nume] [durata] [timing] [delay] [count] [direction] [fill-mode];
}

/* Exemplu complet */
.spinner {
  animation: rotate 1s linear 0s infinite normal forwards;
}

Fiecare valoare:

  • nume — numele @keyframes
  • durata — cât durează (500ms, 2s)
  • timing — curba (linear, ease, ease-out...)
  • delay — întârziere înainte de start
  • count — de câte ori (număr sau infinite)
  • direction — normal, reverse, alternate
  • fill-mode — starea dinainte/după animație

animation-direction

  • normal — 0 → 100% (implicit)
  • reverse — 100 → 0%
  • alternate — 0→100, apoi 100→0, repeat
  • alternate-reverse — 100→0, apoi 0→100, repeat
@keyframes bounce {
  from { transform: translateY(0); }
  to   { transform: translateY(-20px); }
}

.ball {
  animation: bounce 500ms ease-in-out infinite alternate;
  /* sus → jos → sus → jos... */
}

animation-fill-mode — ce păstrează

  • none (implicit) — revine la starea CSS originală după
  • forwards — păstrează starea finală (100%)
  • backwards — aplică starea inițială (0%) și înainte de start
  • both — ambele
@keyframes slideIn {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

.element {
  animation: slideIn 500ms ease-out forwards;
  /* rămâne la opacity 1, translateY 0 după animație */
}

Important: fără forwards, elementul revine invizibil după animație. Aproape mereu folosești forwards pentru animații de intrare.

Exemple practice

Spinner de loading

@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #eee;
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

Fade + slide in

@keyframes fadeSlideIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: fadeSlideIn 600ms ease-out forwards;
}

Animație stagger (cascadă)

.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 100ms; }
.card:nth-child(3) { animation-delay: 200ms; }
.card:nth-child(4) { animation-delay: 300ms; }
.card:nth-child(5) { animation-delay: 400ms; }

Fiecare card apare cu un mic delay față de cel anterior. Efect de cascadă, foarte plăcut vizual.

Animation vs Transition — când care

Două unelte similare, cazuri diferite:

  • Transition — schimbare între 2 stări (hover, click, focus)
  • Animation — secvență automată, loop-uri, animații de intrare

Pentru hover, transition. Pentru spinner infinite, animation. Pentru element care apare la încărcare, animation cu forwards.

animation-play-state — pauză

.carousel {
  animation: scroll 20s linear infinite;
}

.carousel:hover {
  animation-play-state: paused;
}

Carousel care se oprește când mouse-ul e deasupra. Util pentru a da utilizatorului control.

Exemplu explicat
Secțiune hero cu animații în cascadă
@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hero h1 {
  animation: fadeUp 700ms ease-out 100ms forwards;
  opacity: 0; /* start invizibil */
}

.hero p {
  animation: fadeUp 700ms ease-out 300ms forwards;
  opacity: 0;
}

.hero .cta {
  animation: fadeUp 700ms ease-out 500ms forwards;
  opacity: 0;
}
Titlul apare primul (100ms delay), paragraful la 300ms, CTA la 500ms. Fiecare cu animația completă de 700ms. Rezultatul: intro în cascadă, profesional, fără JavaScript.
Alege tool-ul
Vrei un efect de „puls" (se mărește și se micșorează la infinit) pe un buton pentru a atrage atenția. Ce folosești?
Recapitulare
  • @keyframes nume { 0% {...} 50% {...} 100% {...} }
  • animation: nume durată timing delay count direction fill-mode
  • fill-mode: forwards păstrează starea finală
  • direction: alternate pentru bounce back-and-forth
  • infinite pentru loop continuu
  • Stagger cu animation-delay pe :nth-child
  • Transition pentru stări, Animation pentru secvențe
Modulul 8 · Lecția 48 min citire

Animații pe scroll

Ai văzut site-uri unde elementele apar frumos când scrollezi la ele? Până recent, asta cerea JavaScript și o librărie (AOS, GSAP). Acum CSS pur poate face asta cu un feature nou: scroll-driven animations.

Problema

Vrei ca un element să se anime când devine vizibil în viewport. Cu CSS tradițional, @keyframes pornește la încărcarea paginii — nu la scroll. Soluția veche: JavaScript cu IntersectionObserver.

Soluția modernă — animation-timeline: view()

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: translateY(0); }
}

.animate-on-scroll {
  animation: fadeIn linear;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

Când elementul intră în viewport, animația se joacă. Nu la încărcare, ci la scroll. Zero JavaScript.

Cum funcționează

animation-timeline: view() spune animației: „urmărește poziția elementului față de viewport". Animația avansează pe măsură ce elementul intră în ecran.

animation-range definește când începe și se termină animația:

  • entry 0% — când elementul abia începe să intre în viewport
  • entry 100% — când elementul e complet vizibil
  • exit 0% — când începe să iasă
  • exit 100% — când e complet ieșit
/* Animație care se joacă pe măsură ce elementul intră în ecran */
.fade-up {
  animation: fadeIn linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

Efecte comune

Fade-up la scroll

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: translateY(0); }
}

.scroll-fadeup {
  animation: fadeUp linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

Progress bar de reading

@keyframes progress {
  from { width: 0; }
  to   { width: 100%; }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: var(--color-accent);

  animation: progress linear;
  animation-timeline: scroll();
}

scroll() urmărește scroll-ul întregii pagini. Progress bar-ul se întinde proporțional cu cât ai scrollat.

Parallax subtil

@keyframes parallax {
  from { transform: translateY(0); }
  to   { transform: translateY(-100px); }
}

.hero-image {
  animation: parallax linear;
  animation-timeline: view();
}

Imaginea se mișcă în sus mai lent decât restul paginii. Efect parallax fără bibliotecă.

Suport browser

Scroll-driven animations sunt feature-uri noi (2023-2024). Suport:

  • Chrome 115+ — da
  • Edge 115+ — da
  • Safari și Firefox — parțial sau experimental

Pentru site-uri care trebuie să meargă peste tot, folosește @supports sau oferă fallback cu JavaScript (IntersectionObserver).

Fallback cu JavaScript

Dacă ai nevoie de suport universal, pattern-ul JavaScript:

<!-- HTML -->
<article class="fade-in-on-scroll">...</article>

<script>
const elements = document.querySelectorAll('.fade-in-on-scroll');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
    }
  });
}, { threshold: 0.1 });

elements.forEach(el => observer.observe(el));
</script>
.fade-in-on-scroll {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 700ms, transform 700ms;
}

.fade-in-on-scroll.visible {
  opacity: 1;
  transform: translateY(0);
}

IntersectionObserver e API-ul browser-ului pentru „când elementul intră în viewport". CSS face tranziția când se adaugă clasa .visible.

Abordare progresiv-enhanced

/* Base — vizibil direct, pentru utilizatori fără JS sau browser vechi */
.card {
  opacity: 1;
}

/* Doar dacă browser-ul suportă scroll timelines */
@supports (animation-timeline: view()) {
  .card {
    animation: fadeUp linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
}
Exemplu explicat
Cards care apar în cascadă la scroll
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@supports (animation-timeline: view()) {
  .feature-card {
    animation: slideUp linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }

  /* Stagger între ele */
  .feature-card:nth-child(1) { animation-delay: 0ms; }
  .feature-card:nth-child(2) { animation-delay: 100ms; }
  .feature-card:nth-child(3) { animation-delay: 200ms; }
}
Pe măsură ce scrollezi către secțiunea de features, fiecare card apare în cascadă. Browser-uri care nu suportă văd cards-urile normale (fallback gratuit). Progress enhancement la maximum.
Tehnica potrivită
Vrei ca pe site-ul tău să apară fade-in elementele pe măsură ce utilizatorul scroll-ează. Ce abordare e cea mai bună?
Recapitulare
  • animation-timeline: view() = animație legată de scroll
  • animation-range = când începe/se termină (entry, exit, cover)
  • scroll() pentru progress bars (întreaga pagină)
  • Suport modern browser în 2024 — cu fallback
  • Fallback universal: IntersectionObserver + transition CSS
  • Abordare progresivă: @supports pentru browsere vechi
Modulul 8 · Lecția 56 min citire

prefers-reduced-motion

Unii utilizatori au condiții (vertigo, tulburări vestibulare, migrene) unde animațiile provoacă real disconfort — uneori greață. Sistemul lor operațional are o setare „reduce motion". Tu, ca dev, trebuie să respecți acea setare. E o lecție scurtă dar esențială.

Problema

Parallax masiv, elemente care zboară, carusele care se învârt — pentru majoritate sunt plăcute. Pentru un procent mic, provoacă rău fizic. Aceștia își setează sistemul pe „reduce motion" și se așteaptă ca site-urile să respecte setarea.

Media query-ul

@media (prefers-reduced-motion: reduce) {
  /* Stilurile astea se aplică DOAR pentru utilizatorii cu
     „reduce motion" activat în sistem */

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Pattern-ul standard: reduci toate animațiile la aproape instant (0.01ms). Tehnic nu sunt animații, dar browser-ul încă respectă finalizarea.

Abordare mai nuanțată

Uneori vrei să păstrezi anumite animații esențiale (fade-uri scurte) dar să elimini cele mari (parallax, bounce):

/* Animații mici — ok pentru toată lumea */
.button {
  transition: background 150ms;
}

/* Animații mari — dezactivate pentru reduced motion */
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 300ms;
  }

  .card:hover {
    transform: translateY(-6px);
  }

  .hero-image {
    animation: floatUp 3s infinite alternate;
  }
}

no-preference = „utilizatorul NU a cerut reduce motion". Pui animațiile mari înăuntru. Dacă preferința e „reduce", sar peste tot.

Ce să eviți în mod special

  • Parallax — cel mai rău pentru vertigo
  • Mișcări orizontale mari — carusele lente, slide-in-uri lungi
  • Rotații peste 180°
  • Zoom dramatic — scale(0.5) → scale(1.5)
  • Bouncing — elemente care sar repetitiv

Ce e de obicei ok

  • Fade-uri (opacity 0 → 1) — fără mișcare fizică
  • Color transitions
  • Micro-mișcări (translate 2-4px)
  • Animații scurte (sub 200ms)
Testarea

Cum să vezi cum arată site-ul tău pentru utilizatorii cu reduce motion:

  • Mac: System Preferences → Accessibility → Display → Reduce motion
  • Windows: Settings → Ease of Access → Display → Show animations in Windows (off)
  • Chrome DevTools: Command Palette (Cmd+Shift+P) → „Emulate CSS prefers-reduced-motion: reduce"
Exemplu explicat
Setup responsabil pentru animații
/* Base — fără animații în exces */
.card {
  transition: background 150ms;
}

/* Animații complete doar pentru cine nu a cerut reducere */
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition:
      background 150ms,
      transform 300ms ease-out,
      box-shadow 300ms ease-out;
  }

  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 24px rgba(0,0,0,0.1);
  }

  .hero-title {
    animation: fadeUp 700ms ease-out forwards;
    opacity: 0;
  }
}

/* Pentru reduced motion — doar o tranziție instant de opacity */
@media (prefers-reduced-motion: reduce) {
  .hero-title {
    opacity: 1; /* direct vizibil, fără animație */
  }
}
Hover-ul pe card doar schimbă culoarea pentru reduced motion. Animațiile de intrare sunt dezactivate. Pentru restul utilizatorilor, totul arată lin și profesional.
Cât de important
Ar trebui să respecți prefers-reduced-motion chiar dacă doar 1% din utilizatori îl activează?
Recapitulare
  • @media (prefers-reduced-motion: reduce) = utilizatorul a cerut reducere
  • @media (prefers-reduced-motion: no-preference) = ok să animezi normal
  • Pattern comun: resetezi toate animațiile la 0.01ms pentru reduce
  • Evită parallax, rotații mari, bouncing dramatic
  • Fade-uri și color transitions sunt de obicei ok
  • Testează cu System Preferences sau Chrome DevTools
Modulul 8 · Lecția 68 min citire

Micro-interacțiuni

Ce separă un site „ok" de unul „wow"? De obicei, nu design-ul mare — ci detaliile mici. Un buton care se „apasă" la click. Un checkbox care are un pulse când e bifat. Un toast care alunecă în colț când trimiți un form. Astea sunt micro-interacțiunile.

Ce e o micro-interacțiune

Un efect vizual scurt care oferă feedback la acțiunile utilizatorului:

  • Click pe buton → buton se „apasă"
  • Hover pe card → card se ridică
  • Check pe checkbox → pulse scurt
  • Form submit → buton afișează spinner
  • Toggle pe switch → alunecare lină

Niciuna nu e obligatorie. Toate combinate fac site-ul să se simtă „viu" și profesional.

Principiile unei micro-interacțiuni bune

  1. Scurtă — 100-300ms maxim
  2. Subtilă — nu distrage, ajută
  3. Funcțională — oferă feedback relevant
  4. Consistentă — aceleași pattern-uri peste tot

Butonul modern

.btn {
  padding: 0.75rem 1.5rem;
  background: var(--color-accent);
  color: white;
  border-radius: 8px;
  border: none;
  cursor: pointer;

  transition:
    background 150ms,
    transform 100ms,
    box-shadow 200ms;
}

.btn:hover {
  background: var(--color-accent-hover);
  box-shadow: 0 4px 12px rgba(200, 85, 61, 0.3);
}

.btn:active {
  transform: translateY(1px) scale(0.98);
  box-shadow: 0 1px 2px rgba(200, 85, 61, 0.3);
}

.btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

Toggle switch

.switch {
  width: 44px;
  height: 24px;
  background: #ddd;
  border-radius: 100px;
  position: relative;
  cursor: pointer;
  transition: background 200ms;
}

.switch::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  background: white;
  border-radius: 50%;
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}

.switch.active {
  background: var(--color-accent);
}

.switch.active::before {
  transform: translateX(20px);
}

Ripple effect — efect de undă la click

.btn-ripple {
  position: relative;
  overflow: hidden;
}

.btn-ripple::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 200%;
  aspect-ratio: 1;
  background: rgba(255,255,255,0.3);
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(0);
  transition: transform 400ms ease-out;
}

.btn-ripple:active::after {
  transform: translate(-50%, -50%) scale(1);
  transition: 0s;
}

Input focus

.input-group {
  position: relative;
}

.input-group input {
  padding: 1rem 0.75rem 0.5rem;
  border: 1px solid #ddd;
  border-radius: 6px;
  width: 100%;
  transition: border-color 200ms;
}

.input-group label {
  position: absolute;
  top: 50%;
  left: 0.75rem;
  transform: translateY(-50%);
  color: #888;
  pointer-events: none;
  transition: transform 200ms, color 200ms, font-size 200ms;
}

.input-group input:focus {
  border-color: var(--color-accent);
  outline: none;
}

.input-group input:focus + label,
.input-group input:not(:placeholder-shown) + label {
  transform: translateY(-140%);
  font-size: 0.75rem;
  color: var(--color-accent);
}

„Floating label" — label-ul se ridică când input-ul primește focus sau are text. Detaliu de design clasic și util.

Loading state pe buton

@keyframes spin {
  to { transform: rotate(360deg); }
}

.btn.loading {
  color: transparent;
  pointer-events: none;
}

.btn.loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border: 2px solid rgba(255,255,255,0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 600ms linear infinite;
}
Prea multă mișcare = rău

Tendința începătorilor: „să animăm TOTUL". Rezultat: site-ul pare atacat de hiperactivitate.

Regula: 2-3 micro-interacțiuni pe viewport. Butoane, iconițe importante, cards principale. Restul rămân statice. Contrastul cu mișcările subtile e ce dă impact.

Icon care se transformă

.hamburger {
  width: 24px;
  height: 18px;
  position: relative;
  cursor: pointer;
}

.hamburger span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transition: transform 300ms, opacity 200ms;
}

.hamburger span:nth-child(1) { top: 0; }
.hamburger span:nth-child(2) { top: 8px; }
.hamburger span:nth-child(3) { top: 16px; }

.hamburger.active span:nth-child(1) {
  top: 8px;
  transform: rotate(45deg);
}

.hamburger.active span:nth-child(2) {
  opacity: 0;
}

.hamburger.active span:nth-child(3) {
  top: 8px;
  transform: rotate(-45deg);
}

Hamburger menu care se transformă în X când e deschis. Efect clasic care spune „meniul e activ" fără cuvinte.

Exemplu explicat
Card cu multiple micro-interacțiuni
.like-btn {
  width: 40px;
  height: 40px;
  background: transparent;
  border: 1px solid #ddd;
  border-radius: 50%;
  cursor: pointer;
  position: relative;
  transition: all 200ms;
}

.like-btn:hover {
  background: #fff0ee;
  border-color: var(--color-accent);
  transform: scale(1.1);
}

.like-btn.liked {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: white;
  animation: likeBounce 400ms ease-out;
}

@keyframes likeBounce {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.25); }
}

/* Confetti micro — doar un puls simplu extra */
.like-btn.liked::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 2px solid var(--color-accent);
  border-radius: 50%;
  animation: pulseRing 600ms ease-out;
}

@keyframes pulseRing {
  to {
    transform: scale(1.5);
    opacity: 0;
  }
}
Like button cu 3 stări: idle, hover (scale up), liked (bounce + pulse ring). Fiecare stare are propria micro-interacțiune. Satisfăcător la apăsat.
Principiu corect
Care e durata optimă pentru o micro-interacțiune la click pe buton?
Recapitulare
  • Micro-interacțiuni = feedback vizual scurt la acțiuni
  • Scurte (100-300ms), subtile, funcționale
  • Butoane: hover + active states cu transform + shadow
  • Input focus: floating labels
  • Loading state: spinner din text transparent
  • 2-3 micro-interacțiuni per viewport — nu mai multe
  • Consistență: același pattern de hover peste tot în site
Modulul 8 · Lecția 712 min citire

Proiect: landing animat

Luăm landing page-ul din Modulul 6 și îl facem viu cu animații elegante. Fără exces, doar detalii care îl ridică de la „ok" la „wow". Respectăm accesibilitatea cu prefers-reduced-motion.

Ce adăugăm

  • Hero cu animații în cascadă (titlu, subtitlu, butoane)
  • Feature cards care apar la scroll
  • Buton CTA cu pulse subtil
  • Hover effects rafinate peste tot
  • Progress bar de reading
  • Toate cu prefers-reduced-motion fallback

Pasul 1: Animații keyframe de bază

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeInScale {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes pulse {
  0%, 100% {
    transform: scale(1);
    box-shadow: 0 4px 12px rgba(200, 85, 61, 0.3);
  }
  50% {
    transform: scale(1.03);
    box-shadow: 0 6px 20px rgba(200, 85, 61, 0.4);
  }
}

Pasul 2: Hero în cascadă

/* Înveluim toate animațiile în no-preference */
@media (prefers-reduced-motion: no-preference) {
  .hero h1,
  .hero .lead,
  .hero-ctas {
    opacity: 0;
    animation: fadeUp 800ms ease-out forwards;
  }

  .hero h1 {
    animation-delay: 100ms;
  }

  .hero .lead {
    animation-delay: 300ms;
  }

  .hero-ctas {
    animation-delay: 500ms;
  }

  /* CTA principală pulsează subtil */
  .btn-primary {
    animation: pulse 2s ease-in-out infinite;
    animation-delay: 1.5s; /* după ce termină fade-up-ul */
  }
}

Pasul 3: Features la scroll

@media (prefers-reduced-motion: no-preference) {
  .feature-card {
    animation: fadeUp linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }

  /* Stagger prin nth-child */
  .feature-card:nth-child(1) { animation-delay: 0ms; }
  .feature-card:nth-child(2) { animation-delay: 80ms; }
  .feature-card:nth-child(3) { animation-delay: 160ms; }
}

/* Fallback pentru browsere care nu suportă animation-timeline */
@supports not (animation-timeline: view()) {
  /* Vor fi vizibile direct, fără animație la scroll */
  .feature-card {
    opacity: 1;
  }
}

Pasul 4: Hover effects consistente

/* Butoane cu toate stările */
.btn {
  transition:
    background 150ms,
    transform 100ms,
    box-shadow 200ms;
}

@media (prefers-reduced-motion: no-preference) {
  .btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(200, 85, 61, 0.35);
  }

  .btn-primary:active {
    transform: translateY(0);
    transition: 50ms;
  }

  .btn-outline:hover {
    background: var(--color-text);
    color: var(--color-bg);
    transform: translateY(-1px);
  }
}

/* Feature cards */
.feature-card {
  transition:
    transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 300ms ease-out,
    border-color 200ms;
}

@media (prefers-reduced-motion: no-preference) {
  .feature-card:hover {
    transform: translateY(-6px);
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
    border-color: var(--color-accent);
  }
}

/* Icon-ul din card pulsează subtil la hover */
.feature-icon {
  transition: transform 200ms ease-out;
}

.feature-card:hover .feature-icon {
  transform: scale(1.1) rotate(-5deg);
}

Pasul 5: Progress bar de reading

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: var(--color-accent);
  z-index: 100;
  width: 0;
}

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .reading-progress {
      animation: progressGrow linear;
      animation-timeline: scroll();
    }
  }
}

@keyframes progressGrow {
  from { width: 0; }
  to { width: 100%; }
}

Pasul 6: Navbar cu scroll effect

/* Când scrollezi, navbar-ul devine mai opac și primește shadow */
.navbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(251, 250, 247, 0.85);
  backdrop-filter: blur(10px);
  transition: box-shadow 300ms, background 300ms;
}

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .navbar {
      animation: navbarShadow linear;
      animation-timeline: scroll();
      animation-range: 0 100px;
    }
  }
}

@keyframes navbarShadow {
  to {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
    background: rgba(251, 250, 247, 0.95);
  }
}

Pasul 7: Link-uri cu underline animat

.nav-links a {
  position: relative;
  padding: 0.5rem 0;
  transition: color 200ms;
}

.nav-links a::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-reduced-motion: no-preference) {
  .nav-links a:hover::after {
    transform: scaleX(1);
    transform-origin: left;
  }
}

.nav-links a:hover {
  color: var(--color-accent);
}

Underline-ul se întinde de la stânga la dreapta la hover, se retrage tot de la stânga când scoți mouse-ul. Detalii elegante.

Balance în final

Codul pare mult, dar aplică 5 principii simple:

  1. Intro animat — cascadă de 3-4 elemente
  2. Scroll reveal — cards apar pe măsură ce apar în viewport
  3. Hover peste tot — cu aceeași filosofie (ușoară ridicare + shadow)
  4. Progress indicators — reading progress, navbar shadow
  5. Respect pentru accesibilitate — tot înăuntrul prefers-reduced-motion: no-preference

Totul orchestrat, nimic exagerat. Asta e balanța.

Recapitulare
  • Intro cu cascadă — delay-uri incrementale (100ms, 300ms, 500ms)
  • Scroll reveal cu animation-timeline (+ fallback pentru browsere vechi)
  • Hover effects consistente peste tot — ridicare + shadow
  • Micro-interacțiuni: icon la hover, underline animat
  • Progress bar de reading — pur CSS
  • Tot înveluit în @media (prefers-reduced-motion: no-preference)
  • Transform + opacity peste tot = GPU-accelerated, fluid

🎉 Ai terminat Modulul 8!
Pagina ta prinde viață. Urmează Modulul 9 — Forms stilizate. Cel mai subestimat skill din CSS.

Modulul 9 · Lecția 19 min citire

Toate tipurile de input

HTML are 20+ tipuri de input — fiecare cu propriile super-puteri. Alege tipul potrivit și browser-ul îți face jumătate din muncă: validare, tastaturi mobile potrivite, UI specializat. Alege greșit și utilizatorii se chinuie să completeze form-ul.

Text-uri de bază

type="text"

<input type="text" name="nume">

Default-ul. Orice text scurt — nume, titluri, căutări.

type="email"

<input type="email" name="email" required>

Validează automat format de email. Pe mobil, activează tastatura cu @. Folosește-l mereu pentru adrese de email.

type="password"

<input type="password" name="parola" minlength="8">

Ascunde caracterele. Activează integrarea cu password managers (1Password, Bitwarden).

type="tel"

<input type="tel" name="telefon">

Pe mobil, activează tastatura numerică. Nu validează format specific — formatele de telefon variază per țară.

type="url"

<input type="url" name="site">

Validează format URL (cere https://). Pe mobil, activează tastatura cu „.com", „/", „:".

Numere și date

type="number"

<input type="number" name="cantitate" min="1" max="100" step="1">

Afișează săgeți up/down. Atribute: min, max, step.

type="range"

<input type="range" min="0" max="100" value="50">

Slider. Util pentru volum, brightness, orice valoare continuă.

type="date"

<input type="date" name="data-nastere" max="2006-01-01">

Date picker nativ. Browser-ul afișează calendar. Diferite tipuri pentru diverse granularități:

  • type="date" — doar data
  • type="time" — doar ora
  • type="datetime-local" — data și ora
  • type="month" — doar luna
  • type="week" — doar săptămâna

Tipuri speciale

type="checkbox"

<input type="checkbox" id="accept" name="accept">
<label for="accept">Sunt de acord</label>

Bifare da/nu. Multiple checkbox-uri cu același name permit selecție multiplă.

type="radio"

<input type="radio" id="s" name="dim" value="s">
<label for="s">S</label>
<input type="radio" id="m" name="dim" value="m">
<label for="m">M</label>
<input type="radio" id="l" name="dim" value="l">
<label for="l">L</label>

Selecție unică dintr-un grup. Toate radio-urile din același grup au același name.

type="color"

<input type="color" value="#c8553d">

Color picker nativ.

type="file"

<input type="file" accept="image/*" multiple>
  • accept — ce tipuri (image/*, .pdf, .jpg,.png)
  • multiple — selecție multiplă

type="search"

<input type="search" placeholder="Caută...">

Ca text, dar browser-ul afișează buton de „clear" în unele cazuri. Pe iOS, afișează un icon de search.

Textarea — text multi-linie

<textarea name="mesaj" rows="5" cols="40" placeholder="Mesajul tău...">
</textarea>

rows și cols setează dimensiunile default. Utilizatorul poate redimensiona (de obicei din colțul dreapta-jos).

Select — dropdown

<select name="tara">
  <option value="">Alege o țară</option>
  <option value="ro">România</option>
  <option value="md">Moldova</option>
  <option value="us">SUA</option>
</select>

Pentru selecție din listă. Cu <optgroup> poți grupa opțiunile.

Folosește tipul potrivit mereu

Cea mai comună greșeală: type="text" pentru tot. Consecințele:

  • Email fără validare — utilizatorii trimit „tom@" sau „tom.com"
  • Telefon cu tastatură alfabetică pe mobil (enervant)
  • Parolă vizibilă (nu ar trebui)
  • Data introdusă cu format inconsistent

Fiecare tip activează UI specializat și validare. Costul: zero. Beneficiul: major.

Atribute importante

  • required — trebuie completat
  • disabled — dezactivat, nu se poate interacționa
  • readonly — afișat dar nu editabil
  • placeholder — text hint în input
  • value — valoare inițială
  • autocomplete — ajută browser-ul să sugereze (on/off/nume specifice)
  • minlength / maxlength — limite caractere
  • pattern — regex de validare
Exemplu explicat
Formular de signup tipic cu tipurile potrivite
<form>
  <label for="email">Email</label>
  <input
    type="email"
    id="email"
    name="email"
    required
    autocomplete="email"
    placeholder="tu@exemplu.ro"
  >

  <label for="pass">Parolă</label>
  <input
    type="password"
    id="pass"
    name="password"
    required
    minlength="8"
    autocomplete="new-password"
  >

  <label for="age">Vârstă</label>
  <input
    type="number"
    id="age"
    name="age"
    min="18"
    max="120"
  >

  <label for="dob">Data nașterii</label>
  <input
    type="date"
    id="dob"
    name="birthdate"
    max="2006-01-01"
  >
</form>
Fiecare câmp cu tipul potrivit: email → validare + tastatură mobile; password → ascundere + integrare manager; number cu min/max; date cu limită de vârstă (18+). Browser-ul face mult.
Tipul potrivit
Ce tip de input folosești pentru adresa de email într-un form de înregistrare?
= validare automată format + tastatură mobilă optimizată (cu @). Browser-ul nu va lăsa utilizatorul să trimită form-ul cu o adresă greșită ca „tom@.com\". type=\"text\" ar funcționa dar fără validare și fără optimizare mobile. url e pentru URL-uri, search pentru căutări." data-msg-wrong="Nu chiar. Răspunsul corect e B. type=\"email\" activează validarea și tastatura potrivită.">
Recapitulare
  • 20+ tipuri de input, fiecare cu super-puteri
  • Folosește type potrivit: email, tel, url, number, date — nu doar text
  • Beneficii: validare + tastatură mobilă potrivită + UI specializat
  • Atribute importante: required, pattern, min/max, autocomplete
  • Checkbox multiple = nume identic pentru selecție multiplă
  • Radio = nume identic pentru selecție unică
Modulul 9 · Lecția 210 min citire

Stilizarea completă

Input-urile default ale browser-ului sunt urâte — fiecare browser le afișează diferit. Chrome pe Mac arată altfel decât Firefox pe Windows. Stilizarea completă dă consistență și polish. Pattern-ul e același pentru orice design system.

Reset-ul pentru forms

Browserele dau form elements stiluri default care trebuie „sparse":

/* Reset pentru inputs și butoane */
input, button, textarea, select {
  font: inherit; /* moștenește fontul paginii */
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

/* Scoatem stilul default iOS pe iOS */
input, textarea, select, button {
  -webkit-appearance: none;
  appearance: none;
  border-radius: 0;
}

Fără reset-ul ăsta, input-urile vor arăta nativ (rotunjiri iOS, gradient-uri Chrome) — inconsistent cu design-ul tău.

Styling de bază pentru input

input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
input[type="number"],
textarea {
  width: 100%;
  padding: 0.75rem 1rem;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--color-text);
  background: white;
  border: 1px solid #e0dcd2;
  border-radius: 8px;
  transition: border-color 200ms, box-shadow 200ms;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.1);
}

Detalii importante:

  • width: 100% — input-ul ocupă spațiul disponibil
  • outline: none e ok DOAR dacă ai alt indicator de focus (box-shadow)
  • box-shadow la focus = inel subtil color accent
  • Transition scurt pentru schimbare lină

Grup label + input

.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-bottom: 1.25rem;
}

.form-group label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text);
}

.form-group .hint {
  font-size: 0.8125rem;
  color: var(--color-text-soft);
  margin-top: 0.25rem;
}
<div class="form-group">
  <label for="email">Email</label>
  <input type="email" id="email" name="email">
  <p class="hint">Nu vom trimite spam.</p>
</div>

Stilizare select

Select-urile sunt cele mai dificil de stilizat consistent. Pattern-ul modern:

select {
  width: 100%;
  padding: 0.75rem 2.5rem 0.75rem 1rem;
  font-size: 1rem;
  background: white;
  border: 1px solid #e0dcd2;
  border-radius: 8px;
  cursor: pointer;

  /* Scoate săgeata default și o adaugă custom */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
}

select:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.1);
}

Săgeata custom în SVG e inline ca data URL. Poți customiza culoarea schimbând stroke='%23666' (666 = gri).

Textarea

textarea {
  width: 100%;
  min-height: 120px;
  padding: 0.75rem 1rem;
  font-size: 1rem;
  font-family: inherit; /* să nu devină monospace implicit */
  line-height: 1.5;
  border: 1px solid #e0dcd2;
  border-radius: 8px;
  resize: vertical; /* doar pe vertical */
}

resize: vertical permite utilizatorului să mărească doar pe verticală (nu pe orizontală — ar rupe layout-ul).

Stări speciale

/* Input disabled */
input:disabled,
textarea:disabled {
  background: #f5f2ec;
  color: #9d9488;
  cursor: not-allowed;
  opacity: 0.7;
}

/* Input readonly */
input:read-only {
  background: #f9f7f2;
  cursor: default;
}

/* Placeholder */
input::placeholder {
  color: #b0a69a;
  font-style: italic;
}

Auto-fill (background galben Chrome)

Chrome pune un fundal galben pe input-urile auto-completate. Pentru a-l păstra consistent cu design-ul tău:

input:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 1000px white inset;
  -webkit-text-fill-color: var(--color-text);
  transition: background-color 5000s; /* trick ca să nu se schimbe */
}
Nu scoate outline-ul fără înlocuire

Greșeala clasică de accesibilitate: input:focus { outline: none; } fără nimic în loc.

Utilizatorii care navighează cu tastatura (Tab prin form) nu mai văd unde sunt. Devine imposibil de utilizat.

Regula: dacă scoți outline, adaugă box-shadow sau border-color distinct la :focus. Utilizatorii trebuie să vadă mereu ce e focusat.

Exemplu explicat
Form complet stilizat
:root {
  --input-border: #e0dcd2;
  --input-border-focus: var(--color-accent);
  --input-bg: white;
  --input-bg-disabled: #f5f2ec;
}

.form-control {
  width: 100%;
  padding: 0.75rem 1rem;
  font: inherit;
  color: var(--color-text);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 8px;
  transition: border-color 200ms, box-shadow 200ms;
  -webkit-appearance: none;
  appearance: none;
}

.form-control:focus {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.1);
}

.form-control:disabled {
  background: var(--input-bg-disabled);
  color: #9d9488;
  cursor: not-allowed;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-bottom: 1.25rem;
}

.form-label {
  font-size: 0.875rem;
  font-weight: 500;
}

.form-hint {
  font-size: 0.8125rem;
  color: var(--color-text-soft);
}
O singură clasă .form-control aplicată pe toate input-urile. Variabile CSS pentru consistență. Pattern care poate fi folosit peste tot în proiect.
Best practice
Ai folosit outline: none pe inputs să scapi de outline-ul default. Ce trebuie să adaugi în loc?
Recapitulare
  • Reset: font: inherit, appearance: none, border: none
  • Input base: padding, border subtil, border-radius 6-8px
  • :focus cu box-shadow ring = elegant și accesibil
  • Select cu săgeată custom în SVG data URL
  • Textarea cu resize: vertical și font-family: inherit
  • Nu scoate outline fără alt indicator de focus!
Modulul 9 · Lecția 39 min citire

Validare nativă

HTML are validare încorporată. Fără JavaScript. Fără librării. Adaugi required, type="email", pattern — browserul blochează form-ul dacă datele sunt greșite. CSS răspunde la stările de validare. Pentru forms simple, asta e tot ce ai nevoie.

Attribute de validare

required

<input type="email" name="email" required>

Câmpul trebuie completat. Form-ul nu se trimite dacă e gol.

minlength / maxlength

<input type="password" minlength="8" maxlength="64">

min / max (pentru number, date)

<input type="number" min="18" max="120">
<input type="date" min="2025-01-01">

pattern (regex)

<!-- Cod poștal 6 cifre (ex. România) -->
<input type="text" pattern="\d{6}" title="6 cifre">

<!-- Număr telefon România -->
<input type="tel" pattern="(\+40|0)[0-9]{9}">

title e afișat ca mesaj de eroare când pattern-ul nu se potrivește.

Pseudo-classes de validare

CSS știe dacă input-ul e valid sau nu:

  • :valid — input-ul trece validarea
  • :invalid — input-ul nu trece validarea
  • :required — câmp obligatoriu
  • :optional — câmp opțional
  • :user-invalid — invalid ȘI utilizatorul a interacționat cu el (modern)

Evită :invalid pe câmpuri goale

Un bug tipic: utilizatorul abia a deschis pagina, toate câmpurile required sunt deja „invalid" (pentru că sunt goale). Le arăți roșii de la început — agresiv.

Soluția modernă — :user-invalid:

/* Vechea problemă */
input:invalid {
  border-color: red;
}
/* → toate câmpurile sunt roșii la încărcare */

/* Soluția modernă */
input:user-invalid {
  border-color: red;
}
/* → roșu doar după ce utilizatorul a încercat să submit-uieze sau
     a părăsit câmpul fără să-l completeze */

:user-invalid e suportat în browsere moderne din 2023.

Mesaje de eroare custom

Mesajele native ale browser-ului sunt uneori generice. Poți pune text custom:

<input
  type="email"
  required
  title="Te rog introdu o adresă de email validă"
>

Pentru control total, folosești JavaScript cu setCustomValidity(). Dar pentru majoritatea cazurilor, title e suficient.

Stilizare cu feedback

.form-control {
  border: 1px solid #e0dcd2;
  transition: border-color 200ms, box-shadow 200ms;
}

.form-control:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.1);
}

/* Doar după interacțiune */
.form-control:user-invalid {
  border-color: #c53030;
  box-shadow: 0 0 0 3px rgba(197, 48, 48, 0.1);
}

.form-control:user-invalid:focus {
  border-color: #c53030;
  box-shadow: 0 0 0 3px rgba(197, 48, 48, 0.2);
}

/* Success state */
.form-control:user-valid {
  border-color: #38a169;
}

Iconiță în colțul input-ului

.form-group {
  position: relative;
}

.form-group input {
  padding-right: 2.5rem;
}

.form-group::after {
  content: "";
  position: absolute;
  right: 1rem;
  top: 50%;
  width: 20px;
  height: 20px;
  transform: translateY(-50%);
  opacity: 0;
  transition: opacity 200ms;
}

/* Checkmark verde pentru valid */
.form-group:has(input:user-valid)::after {
  content: "✓";
  color: #38a169;
  opacity: 1;
}

/* X roșu pentru invalid */
.form-group:has(input:user-invalid)::after {
  content: "✗";
  color: #c53030;
  opacity: 1;
}
Validare client != validare server

Validarea HTML/CSS se face pe browser — utilizatorul o poate ocoli (poate dezactiva JS sau modifica HTML-ul).

Mereu validează și pe server. Client-side validation e pentru UX rapid (feedback instant). Server-side validation e pentru securitate și date curate.

Regula: nu ai încredere niciodată în input-ul utilizatorului.

Mesaj de eroare sub input

CSS-only error messages cu :has() și :user-invalid:

.form-group .error-msg {
  display: none;
  color: #c53030;
  font-size: 0.8125rem;
  margin-top: 0.25rem;
}

.form-group:has(input:user-invalid) .error-msg {
  display: block;
}
<div class="form-group">
  <label>Email</label>
  <input type="email" required class="form-control">
  <p class="error-msg">Te rog introdu un email valid</p>
</div>
Exemplu explicat
Form cu validare completă și feedback vizual
.form-group {
  position: relative;
  margin-bottom: 1.25rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.375rem;
  font-weight: 500;
}

.form-control {
  width: 100%;
  padding: 0.75rem 2.5rem 0.75rem 1rem;
  border: 1.5px solid #e0dcd2;
  border-radius: 8px;
  transition: border-color 200ms, box-shadow 200ms;
}

.form-control:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.12);
}

/* Error state — doar după interacțiune */
.form-control:user-invalid {
  border-color: #c53030;
  box-shadow: 0 0 0 3px rgba(197, 48, 48, 0.08);
}

/* Success state */
.form-control:user-valid:not(:placeholder-shown) {
  border-color: #38a169;
}

.error-message {
  display: none;
  color: #c53030;
  font-size: 0.8125rem;
  margin-top: 0.375rem;
}

.form-group:has(input:user-invalid) .error-message {
  display: block;
}
Validare fără JavaScript. Feedback instant la blur (părăsirea câmpului) sau la submit. Mesaj de eroare apare doar după ce utilizatorul a interacționat. Green border pentru valid. Toate doar cu CSS.
Alege pseudo-class
Vrei să arăți border roșu pe un input invalid DAR doar după ce utilizatorul a interacționat (nu la încărcarea paginii). Ce folosești?
Recapitulare
  • Attribute de validare: required, pattern, min/max, minlength/maxlength
  • Pseudo-classes: :valid, :invalid, :required, :user-invalid
  • Preferă :user-invalid față de :invalid — doar după interacțiune
  • Mesaje custom cu title pe input
  • Error messages cu :has() — CSS pur
  • Validare client = UX rapid; validare server = securitate
Modulul 9 · Lecția 49 min citire

Checkbox și radio custom

Checkbox-urile și radio-urile default sunt mici, urâte și diferite în fiecare browser. Majoritatea site-urilor profesionale le stilizează custom. Tehnica e simplă odată ce o vezi — ascunzi input-ul original și desenezi unul fake cu CSS.

Tehnica „hide + replace"

Nu poți stiliza checkbox-ul direct (browser-ul îl controlează). Dar poți ascunde input-ul real și stiliza un element vizual care-i ia locul.

<label class="checkbox">
  <input type="checkbox">
  <span class="checkbox-visual"></span>
  <span class="checkbox-label">Sunt de acord</span>
</label>

Input-ul e înăuntrul label-ului. Click-ul pe label = click pe input. Ascundem input-ul, stilizăm span-ul.

CSS-ul complet pentru checkbox custom

.checkbox {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
  user-select: none;
}

/* Ascunde input-ul real, dar nu îl scoate din DOM */
.checkbox input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* Checkbox-ul vizual */
.checkbox-visual {
  width: 20px;
  height: 20px;
  background: white;
  border: 1.5px solid #d0c9bc;
  border-radius: 4px;
  transition: all 200ms;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Checkmark SVG invizibil by default */
.checkbox-visual::after {
  content: "";
  width: 12px;
  height: 12px;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round'><polyline points='20 6 9 17 4 12'/></svg>");
  background-size: contain;
  transform: scale(0);
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Când e bifat, colorează și arată checkmark */
.checkbox input:checked + .checkbox-visual {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.checkbox input:checked + .checkbox-visual::after {
  transform: scale(1);
}

/* Focus ring pentru accesibilitate */
.checkbox input:focus-visible + .checkbox-visual {
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.2);
}

/* Hover pe label */
.checkbox:hover .checkbox-visual {
  border-color: var(--color-accent);
}

Cum funcționează

  • Input-ul e în DOM pentru semantică și tastatură, dar invizibil vizual
  • Span-ul arată ca un checkbox custom
  • Cu input:checked + .checkbox-visual, selectezi span-ul când input-ul e bifat
  • Checkmark-ul animează de la scale(0) la scale(1) — subtle bounce
  • Focus pe input → ring pe span (accesibilitate menținută)

Radio buttons — același pattern

.radio {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
}

.radio input {
  position: absolute;
  opacity: 0;
  width: 0;
}

.radio-visual {
  width: 20px;
  height: 20px;
  background: white;
  border: 1.5px solid #d0c9bc;
  border-radius: 50%; /* rotund, nu pătrat */
  transition: all 200ms;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Punctul interior când e selectat */
.radio-visual::after {
  content: "";
  width: 10px;
  height: 10px;
  background: white;
  border-radius: 50%;
  transform: scale(0);
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.radio input:checked + .radio-visual {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.radio input:checked + .radio-visual::after {
  transform: scale(1);
}

Toggle switch

.switch {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
}

.switch input {
  position: absolute;
  opacity: 0;
}

.switch-track {
  width: 44px;
  height: 24px;
  background: #d0c9bc;
  border-radius: 100px;
  position: relative;
  transition: background 200ms;
}

.switch-track::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  background: white;
  border-radius: 50%;
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.switch input:checked + .switch-track {
  background: var(--color-accent);
}

.switch input:checked + .switch-track::before {
  transform: translateX(20px);
}
<label class="switch">
  <input type="checkbox">
  <span class="switch-track"></span>
  <span>Notificări</span>
</label>
Nu uita accesibilitatea

Oricât de frumoase sunt custom-urile tale, dacă nu respectă accesibilitatea sunt inutile.

Reguli:

  • Nu șterge input-ul din DOM (display: none îl face invizibil pentru screen readers) — folosește tehnica cu opacity: 0; position: absolute
  • Label-ul trebuie să fie conectat la input
  • Trebuie să funcționeze cu Tab + Space/Enter
  • Focus visible indicator e obligatoriu

Checkbox „card" — pentru opțiuni vizuale

Pattern modern pentru alegerea unei opțiuni vizual distincte:

.option-card {
  border: 2px solid #e0dcd2;
  border-radius: 12px;
  padding: 1.25rem;
  cursor: pointer;
  transition: all 200ms;
}

.option-card input {
  position: absolute;
  opacity: 0;
}

.option-card:has(input:checked) {
  border-color: var(--color-accent);
  background: rgba(200, 85, 61, 0.05);
}

.option-card:hover {
  border-color: var(--color-accent);
}
<label class="option-card">
  <input type="radio" name="plan" value="basic">
  <h3>Basic</h3>
  <p>9€/lună</p>
</label>

<label class="option-card">
  <input type="radio" name="plan" value="pro">
  <h3>Pro</h3>
  <p>19€/lună</p>
</label>

Card-uri întregi ca radio buttons. Experiență modernă de selecție, folosit peste tot în aplicații SaaS.

Exemplu explicat
Checkbox custom complet
/* Componenta .checkbox e reutilizabilă în orice formular */
.checkbox {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
  user-select: none;
  font-size: 0.9375rem;
}

.checkbox input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.checkbox-visual {
  width: 20px;
  height: 20px;
  border: 1.5px solid #d0c9bc;
  border-radius: 4px;
  display: grid;
  place-items: center;
  transition: all 180ms;
  flex-shrink: 0;
}

.checkbox-visual::after {
  content: "✓";
  color: white;
  font-size: 14px;
  font-weight: 700;
  opacity: 0;
  transform: scale(0.5);
  transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.checkbox input:checked ~ .checkbox-visual {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.checkbox input:checked ~ .checkbox-visual::after {
  opacity: 1;
  transform: scale(1);
}

.checkbox input:focus-visible ~ .checkbox-visual {
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.2);
}

.checkbox:hover .checkbox-visual {
  border-color: var(--color-accent);
}
Componenta e self-contained și accesibilă. Bounce animation când e bifat (cubic-bezier cu valori >1 la capetele). Focus ring pentru tastatură. Hover pentru mouse. Totul orchestrat în 40 de linii.
Regulă importantă
Cum ascunzi checkbox-ul nativ pentru a-l înlocui vizual, fără să afectezi accesibilitatea?
Recapitulare
  • Tehnica „hide + replace": input invizibil + element vizual
  • Ascunde cu opacity: 0 + position: absolute, NU display: none
  • Stilizează cu input:checked + .visual
  • Animația bounce cu cubic-bezier(0.34, 1.56, 0.64, 1)
  • Focus ring obligatoriu pentru accesibilitate
  • Pattern „option card" cu :has() pentru selectare vizuală
Modulul 9 · Lecția 513 min citire

Proiect: formular contact

Construim un formular de contact complet — cu validare nativă, stări visual, feedback, checkbox custom, totul responsive. E genul de formular pe care-l poți copia pe orice site profesional.

Ce construim

  • Nume și email (required)
  • Telefon (opțional)
  • Tip cerere (radio cu card-uri)
  • Mesaj (textarea)
  • Checkbox acord GDPR
  • Buton submit cu loading state
  • Validare cu feedback vizual

Pasul 1: HTML-ul

<form class="contact-form" novalidate>
  <h2>Contactează-ne</h2>
  <p class="form-lead">Răspundem în maxim 24h</p>

  <div class="form-row">
    <div class="form-group">
      <label for="name">Nume complet</label>
      <input type="text" id="name" class="form-control" required minlength="2">
      <p class="error-msg">Numele e obligatoriu</p>
    </div>

    <div class="form-group">
      <label for="email">Email</label>
      <input type="email" id="email" class="form-control" required>
      <p class="error-msg">Email invalid</p>
    </div>
  </div>

  <div class="form-group">
    <label for="phone">Telefon <span class="optional">(opțional)</span></label>
    <input type="tel" id="phone" class="form-control" pattern="[+0-9\s-]+">
  </div>

  <fieldset class="form-group">
    <legend>Tip cerere</legend>
    <div class="option-cards">
      <label class="option-card">
        <input type="radio" name="type" value="info" required>
        <span class="option-title">Informații</span>
        <span class="option-desc">Detalii despre servicii</span>
      </label>

      <label class="option-card">
        <input type="radio" name="type" value="quote">
        <span class="option-title">Cerere ofertă</span>
        <span class="option-desc">Estimare pentru proiect</span>
      </label>

      <label class="option-card">
        <input type="radio" name="type" value="support">
        <span class="option-title">Suport</span>
        <span class="option-desc">Ajutor tehnic</span>
      </label>
    </div>
  </fieldset>

  <div class="form-group">
    <label for="message">Mesaj</label>
    <textarea id="message" class="form-control" rows="5" required minlength="10"></textarea>
    <p class="error-msg">Mesajul trebuie să aibă cel puțin 10 caractere</p>
  </div>

  <label class="checkbox">
    <input type="checkbox" required>
    <span class="checkbox-visual"></span>
    <span>Sunt de acord cu <a href="#">politica de confidențialitate</a></span>
  </label>

  <button type="submit" class="btn btn-primary">Trimite mesajul</button>
</form>

Pasul 2: Design tokens

:root {
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-bg: #fbfaf7;
  --color-panel: white;
  --color-accent: #c8553d;
  --color-accent-hover: #a8432e;
  --color-border: #e0dcd2;
  --color-error: #c53030;
  --color-success: #38a169;

  --radius: 10px;
  --radius-lg: 14px;

  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;
}

Pasul 3: Structura formularului

.contact-form {
  max-width: 600px;
  margin: 0 auto;
  padding: 2.5rem;
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  font-family: var(--font-sans);
}

.contact-form h2 {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  margin-bottom: 0.5rem;
}

.form-lead {
  color: var(--color-text-soft);
  margin-bottom: 2rem;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

@media (max-width: 600px) {
  .form-row {
    grid-template-columns: 1fr;
  }
}

.form-group {
  margin-bottom: 1.25rem;
}

.form-group label,
.form-group legend {
  display: block;
  font-size: 0.875rem;
  font-weight: 500;
  margin-bottom: 0.375rem;
}

.optional {
  color: var(--color-text-soft);
  font-weight: 400;
}

fieldset {
  border: none;
  padding: 0;
}

Pasul 4: Input-uri și validare

.form-control {
  width: 100%;
  padding: 0.75rem 1rem;
  font: inherit;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius);
  transition: border-color 200ms, box-shadow 200ms;
  -webkit-appearance: none;
  appearance: none;
}

.form-control:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.12);
  background: white;
}

textarea.form-control {
  resize: vertical;
  min-height: 120px;
  font-family: inherit;
  line-height: 1.5;
}

/* Error state */
.form-control:user-invalid {
  border-color: var(--color-error);
}

.form-control:user-invalid:focus {
  box-shadow: 0 0 0 3px rgba(197, 48, 48, 0.1);
}

/* Error message */
.error-msg {
  display: none;
  color: var(--color-error);
  font-size: 0.8125rem;
  margin-top: 0.375rem;
}

.form-group:has(.form-control:user-invalid) .error-msg {
  display: block;
}

Pasul 5: Option cards (radio)

.option-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
}

@media (max-width: 600px) {
  .option-cards {
    grid-template-columns: 1fr;
  }
}

.option-card {
  position: relative;
  padding: 1rem;
  background: var(--color-bg);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: all 200ms;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.option-card input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.option-title {
  font-weight: 600;
  font-size: 0.9375rem;
}

.option-desc {
  font-size: 0.8125rem;
  color: var(--color-text-soft);
}

.option-card:hover {
  border-color: var(--color-accent);
}

.option-card:has(input:checked) {
  border-color: var(--color-accent);
  background: rgba(200, 85, 61, 0.06);
}

.option-card:has(input:focus-visible) {
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.2);
}

Pasul 6: Checkbox custom

.checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  cursor: pointer;
  font-size: 0.875rem;
  margin-bottom: 1.5rem;
  padding: 0.5rem;
  border-radius: var(--radius);
  transition: background 200ms;
}

.checkbox:hover {
  background: rgba(200, 85, 61, 0.03);
}

.checkbox input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.checkbox-visual {
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--color-border);
  border-radius: 4px;
  display: grid;
  place-items: center;
  transition: all 180ms;
  flex-shrink: 0;
  margin-top: 1px;
}

.checkbox-visual::after {
  content: "✓";
  color: white;
  font-size: 13px;
  font-weight: 700;
  opacity: 0;
  transform: scale(0.5);
  transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.checkbox input:checked ~ .checkbox-visual {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.checkbox input:checked ~ .checkbox-visual::after {
  opacity: 1;
  transform: scale(1);
}

.checkbox input:focus-visible ~ .checkbox-visual {
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.2);
}

.checkbox a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

Pasul 7: Butonul submit

.btn {
  display: inline-block;
  padding: 0.875rem 2rem;
  font: inherit;
  font-weight: 500;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  transition: all 200ms;
  position: relative;
}

.btn-primary {
  background: var(--color-text);
  color: white;
  width: 100%;
}

.btn-primary:hover {
  background: var(--color-accent);
  transform: translateY(-1px);
  box-shadow: 0 6px 12px rgba(200, 85, 61, 0.2);
}

.btn-primary:active {
  transform: translateY(0);
}

.btn-primary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.3);
}

/* Loading state */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.btn.loading {
  color: transparent;
  pointer-events: none;
}

.btn.loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 600ms linear infinite;
}

Pasul 8: JavaScript pentru submit

JavaScript minimal pentru a gestiona submit-ul (validare, loading, success):

document.querySelector('.contact-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  const form = e.target;
  const btn = form.querySelector('button[type="submit"]');

  // Validare manuală (checkValidity() e API nativ)
  if (!form.checkValidity()) {
    form.reportValidity(); // arată erorile browser-ului
    return;
  }

  // Loading state
  btn.classList.add('loading');

  try {
    // Aici ar merge fetch() către backend
    await new Promise(resolve => setTimeout(resolve, 1500));

    // Success
    form.reset();
    btn.classList.remove('loading');
    btn.textContent = 'Mesaj trimis ✓';

    setTimeout(() => {
      btn.textContent = 'Trimite mesajul';
    }, 3000);
  } catch (err) {
    btn.classList.remove('loading');
    alert('Eroare: încearcă din nou');
  }
});
Backend e separat

Partea de CSS și HTML e completă. Pentru a face formularul să trimită emailuri reale, ai nevoie de backend. Opțiuni comune:

  • Formspree — no-backend, free tier 50 emailuri/lună
  • Netlify Forms — dacă hostuiești pe Netlify, 100 free/lună
  • Backend propriu — PHP, Node.js, Python cu SendGrid/Mailgun

Pentru CSS-ul nostru, e irelevant ce e în spate — formularul arată și validează identic.

Recapitulare
  • Form complet cu grid pentru rânduri (grid-template-columns: 1fr 1fr)
  • Reset-ul se transformă pe mobile cu media query
  • Validare nativă cu :user-invalid + mesaje via :has()
  • Option cards cu radio invizibil + :has(:checked)
  • Checkbox custom cu bounce animation
  • Loading state cu spinner pur CSS
  • JavaScript minim doar pentru submit (restul e CSS)

🎉 Ai terminat Modulul 9!
Urmează Modulul 10 — proiectul final: construim împreună un portofoliu personal complet.

Modulul 10 · Lecția 110 min citire

Planificare și structură

Ai parcurs 9 module. Ai toate uneltele. Acum construim ceva complet — un portofoliu personal pe care-l poți folosi efectiv. E mai mult decât un exercițiu: e prima bucată de „dovadă" că știi frontend. Orice dev-er junior are nevoie de unul.

Ce construim

Un portofoliu single-page, modern, cu:

  • Hero — numele tău, ce faci, call to action
  • Navbar sticky — link-uri la secțiuni, hamburger pe mobil
  • Proiecte — card-uri cu imagini, titluri, tehnologii folosite
  • Despre — poveste scurtă + skills
  • Contact — formular cu validare
  • Footer — social links

Responsive, animat, accesibil, production-ready. Lecțiile 10-2, 10-3, 10-4 construiesc fiecare parte.

Principiile design-ului

Înainte să scriem cod, câteva principii care fac diferența între „ok" și „impresionant":

1. Un singur obiectiv

Portofoliul are UN scop: să te ajute să primești un job sau un client. Fiecare secțiune trebuie să servească asta. Nu adăuga chestii „pentru că arată bine" dacă nu ajută conversia (apăsarea butonului „Contact" sau trimiterea unui email).

2. Arată, nu spune

„Sunt creativ" nu convinge pe nimeni. Un portofoliu frumos design-at demonstrează creativitatea. Design-ul tău e demonstrația.

3. Constrângeri, nu libertate

Tinerii dev-eri încearcă să folosească TOT ce au învățat. Rezultat: chaos. Portofoliile bune au 3-4 culori, 2 fonturi, spațiere consistentă. Mai puțin e mai mult.

Moodboard-ul tău

Înainte să scrii HTML, decide:

  • Personalitate: cool și tehnic? Cald și prietenos? Elegant și minimalist?
  • Paletă: 2-3 culori de bază + 1 accent. Limitate, distinctive.
  • Tipografie: 1 font display pentru titluri + 1 sans pentru body. Maxim.
  • Tonul textelor: formal sau casual? (Alege tonul care te reprezintă.)

Paleta pentru cursul nostru

Pentru exemplul ghidat, folosim paleta caldă deja introdusă în curs:

:root {
  /* Culori */
  --color-text: #1a1814;
  --color-text-soft: #5c564b;
  --color-text-muted: #8a8578;
  --color-bg: #fbfaf7;
  --color-panel: #ffffff;
  --color-accent: #c8553d;
  --color-accent-hover: #a8432e;
  --color-border: rgba(0, 0, 0, 0.08);

  /* Tipografie */
  --font-display: 'Instrument Serif', Georgia, serif;
  --font-sans: 'Inter Tight', -apple-system, sans-serif;

  /* Fluidă — scalare continuă */
  --fs-body: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fs-lead: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
  --fs-h3: clamp(1.25rem, 1.1rem + 0.7vw, 1.625rem);
  --fs-h2: clamp(2rem, 1.5rem + 2vw, 3rem);
  --fs-h1: clamp(2.5rem, 1.5rem + 4.5vw, 5.5rem);

  /* Spațiere */
  --space-section: clamp(4rem, 8vw, 8rem);
  --space-inline: clamp(1rem, 4vw, 3rem);

  /* Forme */
  --radius: 10px;
  --radius-lg: 14px;
}

Structura HTML-ului

Gândește pagina ca un schelet clar:

<!DOCTYPE html>
<html lang="ro">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- SEO -->
  <title>Alex Ionescu — Frontend Developer</title>
  <meta name="description" content="Frontend developer specializat în React și design systems. Portofoliu și proiecte recente.">

  <!-- Open Graph pentru social media -->
  <meta property="og:title" content="Alex Ionescu — Frontend Developer">
  <meta property="og:description" content="Portofoliu și proiecte recente.">
  <meta property="og:image" content="/og-image.jpg">

  <!-- Favicon -->
  <link rel="icon" href="/favicon.ico">

  <!-- Fonts -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">

  <link rel="stylesheet" href="styles.css">
</head>
<body>

  <header class="navbar"> ... </header>

  <main>
    <section class="hero" id="hero"> ... </section>
    <section class="work" id="work"> ... </section>
    <section class="about" id="about"> ... </section>
    <section class="contact" id="contact"> ... </section>
  </main>

  <footer> ... </footer>

  <script src="main.js"></script>
</body>
</html>

SEO și Open Graph — de ce contează

Când cineva partajează link-ul tău pe Twitter/LinkedIn/WhatsApp, ar trebui să apară un card frumos cu titlu, descriere, imagine. Asta se face cu tag-uri <meta property="og:*">.

  • og:title — titlul care apare în preview
  • og:description — descrierea scurtă
  • og:image — imaginea (1200x630px recomandat)
  • og:url — URL-ul canonic

Creezi og-image.jpg cu designul portofoliului + numele tău + „Frontend Developer". Un singur screenshot bun = mii de impresii când partajezi.

Plan înainte să scrii cod

Cea mai comună greșeală la începători: deschizi editor-ul și scrii HTML random. Ajungi cu cod haotic.

Pas corect: desenează pe hârtie (sau Figma) cum arată fiecare secțiune. Ce e hero-ul? Câte carduri de proiecte? În ce ordine curg secțiunile? Odată ce ai claritate, codul devine o simplă transcriere.

Structura CSS

Organizare recomandată pentru styles.css:

/* === 1. Reset === */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
/* ... */

/* === 2. Design tokens === */
:root {
  /* culori, fonturi, spacing, radius */
}

/* === 3. Base styles === */
body { ... }
h1, h2, h3 { ... }
a { ... }

/* === 4. Layout utilities === */
.container { ... }
.section { ... }

/* === 5. Components === */
.btn { ... }
.card { ... }
.navbar { ... }

/* === 6. Page sections === */
.hero { ... }
.work { ... }
.about { ... }
.contact { ... }

/* === 7. Animations === */
@keyframes fadeUp { ... }

/* === 8. Media queries === */
@media (min-width: 768px) { ... }
Recapitulare
  • Planifică înainte să scrii cod — pe hârtie, în Figma, oriunde
  • Un scop clar: să primești job sau client
  • Constrângeri în design — 2-3 culori, 2 fonturi, simplu
  • Structură HTML semantică: header, main cu sections, footer
  • Tag-uri Open Graph pentru partajare pe social
  • CSS organizat în 8 zone: reset, tokens, base, layout, components, sections, animations, media
Modulul 10 · Lecția 215 min citire

Hero și navbar

Primele 3 secunde contează enorm. Când cineva deschide portofoliul, ce vede în hero decide dacă continuă sau închide tab-ul. Construim un hero care convinge și un navbar care nu distrage.

Navbar-ul — HTML

<header class="navbar">
  <a class="brand" href="#hero">Alex Ionescu</a>

  <button class="menu-toggle" aria-label="Deschide meniu">
    <span></span>
    <span></span>
    <span></span>
  </button>

  <nav class="nav-menu" aria-label="Navigare principală">
    <a href="#work">Proiecte</a>
    <a href="#about">Despre</a>
    <a href="#contact">Contact</a>
  </nav>
</header>

Navbar — CSS

.navbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem var(--space-inline);
  background: rgba(251, 250, 247, 0.85);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--color-border);
}

.brand {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-style: italic;
  color: var(--color-text);
  text-decoration: none;
}

.nav-menu {
  display: flex;
  gap: 2rem;
}

.nav-menu a {
  color: var(--color-text-soft);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.9375rem;
  position: relative;
  transition: color 200ms;
}

.nav-menu a::after {
  content: "";
  position: absolute;
  bottom: -4px;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.nav-menu a:hover {
  color: var(--color-accent);
}

.nav-menu a:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* Hamburger — ascuns pe desktop */
.menu-toggle {
  display: none;
  background: none;
  border: none;
  width: 40px;
  height: 40px;
  cursor: pointer;
  padding: 0;
  flex-direction: column;
  gap: 5px;
  align-items: center;
  justify-content: center;
}

.menu-toggle span {
  display: block;
  width: 24px;
  height: 2px;
  background: var(--color-text);
  transition: transform 300ms, opacity 200ms;
}

/* Mobile — sub 768px */
@media (max-width: 767px) {
  .menu-toggle {
    display: flex;
  }

  .nav-menu {
    position: fixed;
    inset: 0 0 0 50%;
    padding: 5rem 2rem 2rem;
    background: var(--color-bg);
    flex-direction: column;
    gap: 1.5rem;
    transform: translateX(100%);
    transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  .nav-menu.active {
    transform: translateX(0);
  }

  .nav-menu a {
    font-size: 1.25rem;
    font-family: var(--font-display);
  }

  /* Hamburger se transformă în X când meniul e deschis */
  .menu-toggle.active span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
  }

  .menu-toggle.active span:nth-child(2) {
    opacity: 0;
  }

  .menu-toggle.active span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
  }
}

JavaScript minim pentru hamburger

const menuToggle = document.querySelector('.menu-toggle');
const navMenu = document.querySelector('.nav-menu');

menuToggle.addEventListener('click', () => {
  menuToggle.classList.toggle('active');
  navMenu.classList.toggle('active');
});

// Închide meniul când cineva click pe un link
navMenu.querySelectorAll('a').forEach(link => {
  link.addEventListener('click', () => {
    menuToggle.classList.remove('active');
    navMenu.classList.remove('active');
  });
});

Hero-ul — HTML

<section class="hero" id="hero">
  <div class="hero-content">
    <p class="hero-eyebrow">Frontend Developer</p>
    <h1 class="hero-title">
      Construiesc interfețe <em>pe care</em> oamenii le iubesc să le folosească.
    </h1>
    <p class="hero-lead">
      Lucrez cu React, CSS modern și design systems. Bazat în București, disponibil remote.
    </p>
    <div class="hero-cta">
      <a href="#work" class="btn btn-primary">Vezi proiectele</a>
      <a href="#contact" class="btn btn-ghost">Contact →</a>
    </div>
  </div>

  <div class="hero-scroll">
    <span>Derulează</span>
    <div class="scroll-indicator"></div>
  </div>
</section>

Hero-ul — CSS

.hero {
  min-height: calc(100dvh - 80px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: var(--space-section) var(--space-inline);
  position: relative;
}

.hero-content {
  max-width: 900px;
}

.hero-eyebrow {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-accent);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-bottom: 1.5rem;
}

.hero-title {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.03em;
  margin-bottom: 1.5rem;
  color: var(--color-text);
}

.hero-title em {
  font-style: italic;
  color: var(--color-accent);
}

.hero-lead {
  font-size: var(--fs-lead);
  color: var(--color-text-soft);
  max-width: 640px;
  margin: 0 auto 2.5rem;
  line-height: 1.5;
}

.hero-cta {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

Butoanele

.btn {
  display: inline-block;
  padding: 0.875rem 1.75rem;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
  border-radius: var(--radius);
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: all 200ms;
}

.btn-primary {
  background: var(--color-text);
  color: var(--color-bg);
}

.btn-primary:hover {
  background: var(--color-accent);
  transform: translateY(-1px);
  box-shadow: 0 6px 12px rgba(200, 85, 61, 0.25);
}

.btn-ghost {
  color: var(--color-text);
  border-color: var(--color-border);
}

.btn-ghost:hover {
  border-color: var(--color-text);
  background: var(--color-panel);
}

Scroll indicator în hero

.hero-scroll {
  position: absolute;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: var(--color-text-soft);
  font-size: 0.8125rem;
}

.scroll-indicator {
  width: 2px;
  height: 30px;
  background: var(--color-border);
  position: relative;
  overflow: hidden;
}

.scroll-indicator::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 40%;
  background: var(--color-accent);
  animation: scrollHint 2s ease-in-out infinite;
}

@keyframes scrollHint {
  0% { transform: translateY(-100%); }
  100% { transform: translateY(300%); }
}

Animații de intrare

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hero-eyebrow { animation: fadeUp 700ms ease-out 100ms forwards; opacity: 0; }
  .hero-title { animation: fadeUp 700ms ease-out 250ms forwards; opacity: 0; }
  .hero-lead { animation: fadeUp 700ms ease-out 400ms forwards; opacity: 0; }
  .hero-cta { animation: fadeUp 700ms ease-out 550ms forwards; opacity: 0; }
  .hero-scroll { animation: fadeUp 700ms ease-out 800ms forwards; opacity: 0; }
}
Eyebrow — micul mare detaliu

„Eyebrow" (sprânceană) e textul mic, colorat, peste titlu. În exemplu: „Frontend Developer" deasupra titlului mare.

E un detaliu care separă portofoliile profesionale de cele amatorești. Dă context imediat, pregătește privitorul pentru titlul mare, și arată polish.

Format tipic: text scurt (1-3 cuvinte), uppercase, letter-spacing larg, culoare accent.

Ce am construit

Navbar-ul și hero-ul sunt gata. Ce face pattern-ul ăsta special:

  • Navbar sticky cu blur efect (frosted glass)
  • Hamburger care se transformă în X — zero librării
  • Hero cu animații în cascadă
  • Scroll indicator animat care invită la scroll
  • Butoane cu două stiluri (primary și ghost) — o convenție puternică
  • Tipografie fluidă (clamp) care se adaptează la orice ecran
Recapitulare
  • Navbar sticky + backdrop-filter pentru frosted glass
  • Underline animat sub link-uri la hover (transform: scaleX)
  • Hamburger menu cu CSS transforms (span-uri care rotesc în X)
  • Hero cu eyebrow + titlu mare + lead + CTAs
  • Tipografie fluidă cu clamp pe toate dimensiunile
  • Animații în cascadă pe elementele hero
  • Scroll indicator pentru a invita la explorare
Modulul 10 · Lecția 315 min citire

Proiecte și despre

Miezul portofoliului: secțiunea de proiecte. Ce ai construit, pentru cine, cu ce tehnologii. Apoi secțiunea „despre" — nu biografia ta completă, ci ce e relevant pentru job.

Section title pattern

Toate secțiunile de la acest punct folosesc același pattern pentru titlu. Reutilizabilitate:

.section-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin-bottom: 0.75rem;
}

.section-title em {
  font-style: italic;
  color: var(--color-accent);
}

.section-lead {
  font-size: var(--fs-lead);
  color: var(--color-text-soft);
  max-width: 600px;
  margin-bottom: 4rem;
}

.section {
  padding: var(--space-section) var(--space-inline);
  max-width: 1200px;
  margin: 0 auto;
}

Proiecte — HTML

<section class="work section" id="work">
  <h2 class="section-title">Proiecte <em>recente</em></h2>
  <p class="section-lead">Selecția mea de lucrări din ultimii doi ani.</p>

  <div class="projects-grid">

    <article class="project-card">
      <div class="project-image">
        <img src="proj-1.webp" alt="Captură MyShop" width="800" height="500" loading="lazy">
      </div>
      <div class="project-meta">
        <h3 class="project-title">MyShop</h3>
        <p class="project-desc">Magazin online pentru produse handmade. Peste 500 comenzi în primele 3 luni.</p>
        <div class="project-tags">
          <span>Next.js</span>
          <span>Stripe</span>
          <span>Tailwind</span>
        </div>
        <a href="#" class="project-link">Vezi proiectul →</a>
      </div>
    </article>

    <!-- mai multe .project-card -->

  </div>
</section>

Proiecte — CSS

.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 2rem;
}

.project-card {
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 300ms ease-out;
  border: 1px solid var(--color-border);
}

.project-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
}

.project-image {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--color-bg);
}

.project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);
}

.project-card:hover .project-image img {
  transform: scale(1.05);
}

.project-meta {
  padding: 1.5rem 1.75rem 2rem;
}

.project-title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 400;
  margin-bottom: 0.5rem;
}

.project-desc {
  color: var(--color-text-soft);
  font-size: 0.9375rem;
  line-height: 1.55;
  margin-bottom: 1.25rem;
}

.project-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.project-tags span {
  font-size: 0.8125rem;
  padding: 0.25rem 0.75rem;
  background: var(--color-bg);
  color: var(--color-text-soft);
  border-radius: 100px;
  font-weight: 500;
}

.project-link {
  color: var(--color-accent);
  font-weight: 500;
  text-decoration: none;
  font-size: 0.9375rem;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  transition: gap 200ms;
}

.project-link:hover {
  gap: 0.5rem;
}

Detalii care fac diferența

  • aspect-ratio 16:10 — toate imaginile au aceeași proporție, layout-ul rămâne consistent indiferent de imagini
  • img se mărește la hover — efect subtil care semnalează că cardul e clickabil
  • Tags în pill-uri — arată rapid stack-ul tehnic
  • Săgeata „→" se mișcă la hovertransition: gap e un trick elegant
  • auto-fit grid — 1 coloană pe mobil, 2 pe tabletă, 3 pe desktop lat, fără media queries

Despre — HTML

<section class="about section" id="about">
  <h2 class="section-title">Despre <em>mine</em></h2>

  <div class="about-grid">
    <div class="about-text">
      <p class="about-lead">
        Am început să programez la 16 ani, pentru că-mi plăceau jocurile și voiam să le înțeleg. Astăzi, construiesc interfețe pentru startup-uri și agenții.
      </p>
      <p>
        Lucrez cel mai mult cu React și TypeScript, dar am o slăbiciune specială pentru CSS. Cred că un frontend bun nu se cunoaște — se simte: iei o decizie, și pagina răspunde exact cum te așteptai.
      </p>
      <p>
        În afara job-ului: fotografie pe film, fugit (prost, dar constant), și cafea făcută acasă cu aeropress.
      </p>
    </div>

    <div class="about-skills">
      <h3 class="skills-title">Ce folosesc</h3>
      <ul class="skills-list">
        <li>
          <strong>Frontend</strong>
          <span>React, Next.js, TypeScript</span>
        </li>
        <li>
          <strong>Styling</strong>
          <span>CSS modern, Tailwind, design systems</span>
        </li>
        <li>
          <strong>Tooling</strong>
          <span>Vite, Git, Figma</span>
        </li>
        <li>
          <strong>Experiență</strong>
          <span>4 ani, 12+ proiecte live</span>
        </li>
      </ul>
    </div>
  </div>
</section>

Despre — CSS

.about-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 4rem;
  align-items: start;
}

@media (max-width: 767px) {
  .about-grid {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }
}

.about-text {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.about-lead {
  font-size: var(--fs-lead);
  color: var(--color-text);
  line-height: 1.5;
}

.about-text p {
  color: var(--color-text-soft);
  line-height: 1.65;
}

.about-skills {
  background: var(--color-panel);
  padding: 2rem;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
}

.skills-title {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 400;
  margin-bottom: 1.5rem;
}

.skills-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.skills-list li {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--color-border);
}

.skills-list li:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.skills-list strong {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
}

.skills-list span {
  font-size: 0.9375rem;
  color: var(--color-text);
}

Sfaturi pentru text (la fel de important ca design-ul)

Design-ul ține vizitatorul. Textul îl convinge. Câteva reguli:

Folosește „eu", nu „noi"

Dacă ești singur, nu fi corporate: „noi construim" sună fals. „Construiesc" e direct și onest.

Arată rezultate, nu responsibilități

Rău: „Am lucrat la un magazin online." Bun: „Magazin online cu 500+ comenzi în 3 luni." Numărul convinge.

Include personalitate

„În afara job-ului: fotografie pe film, cafea cu aeropress." — detalii care te fac real. Clienții vor să lucreze cu oameni, nu cu CV-uri.

Secțiunea despre e despre vizitator

Greșeala clasică: „despre mine" devine autobiografia completă. Nimeni nu citește asta.

Secțiunea ar trebui să răspundă la întrebarea vizitatorului: „De ce ar trebui să lucrez cu persoana asta?"

3 paragrafe scurte e maxim. Primul: cine ești profesional. Al doilea: cum lucrezi/filosofia ta. Al treilea: ceva personal care te face uman. Stop.

Animații pe scroll pentru carduri

@keyframes slideUp {
  from { opacity: 0; transform: translateY(40px); }
  to { opacity: 1; transform: translateY(0); }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .project-card {
      animation: slideUp linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
  }
}

Cardurile apar pe măsură ce intră în viewport. Browser-uri moderne primesc efectul. Vechi le văd normale (fallback gratuit).

Recapitulare
  • Section-title pattern reutilizabil cu em italic pentru accent
  • Project cards cu aspect-ratio pentru layout consistent
  • Hover zoom pe imagine + ridicare card + săgeata care alunecă
  • Tags ca pill-uri pentru stack tehnic
  • About grid cu 1.5fr : 1fr (text mai lat decât skills)
  • Animații la scroll cu animation-timeline (progresiv)
  • Text: eu > noi, rezultate > responsibilități, personalitate > CV
Modulul 10 · Lecția 414 min citire

Contact, footer și polish

Am ajuns la ultima bucată — contact, footer, și trecem prin detaliile care separă „ok" de „wow". Micile polishuri care fac portofoliul să pară terminat de un profesionist.

Contact — HTML

<section class="contact section" id="contact">
  <div class="contact-grid">
    <div class="contact-intro">
      <h2 class="section-title">Hai să <em>vorbim</em></h2>
      <p class="section-lead">
        Răspund în maxim 24 de ore. Pentru proiecte urgente, preferă telefonul.
      </p>

      <div class="contact-methods">
        <a href="mailto:alex@example.com" class="contact-method">
          <span class="contact-label">Email</span>
          <span class="contact-value">alex@example.com</span>
        </a>
        <a href="tel:+40712345678" class="contact-method">
          <span class="contact-label">Telefon</span>
          <span class="contact-value">+40 712 345 678</span>
        </a>
        <div class="contact-method">
          <span class="contact-label">Locație</span>
          <span class="contact-value">București, România</span>
        </div>
      </div>
    </div>

    <form class="contact-form" novalidate>
      <div class="form-group">
        <label for="cname">Nume</label>
        <input type="text" id="cname" class="form-control" required>
      </div>

      <div class="form-group">
        <label for="cemail">Email</label>
        <input type="email" id="cemail" class="form-control" required>
      </div>

      <div class="form-group">
        <label for="cmessage">Mesaj</label>
        <textarea id="cmessage" class="form-control" rows="5" required minlength="10"></textarea>
      </div>

      <button type="submit" class="btn btn-primary btn-full">
        Trimite mesajul
      </button>
    </form>
  </div>
</section>

Contact — CSS

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 4rem;
  align-items: start;
}

@media (max-width: 767px) {
  .contact-grid {
    grid-template-columns: 1fr;
    gap: 3rem;
  }
}

.contact-methods {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 2rem;
}

.contact-method {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 1rem 1.25rem;
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  text-decoration: none;
  transition: all 200ms;
}

a.contact-method:hover {
  border-color: var(--color-accent);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}

.contact-label {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
}

.contact-value {
  color: var(--color-text);
  font-weight: 500;
  font-size: 0.9375rem;
}

/* Form */
.contact-form {
  background: var(--color-panel);
  padding: 2rem;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
}

.form-group {
  margin-bottom: 1.25rem;
}

.form-group label {
  display: block;
  font-size: 0.875rem;
  font-weight: 500;
  margin-bottom: 0.375rem;
}

.form-control {
  width: 100%;
  padding: 0.75rem 1rem;
  font: inherit;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius);
  transition: all 200ms;
}

.form-control:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.12);
  background: white;
}

.form-control:user-invalid {
  border-color: #c53030;
}

textarea.form-control {
  resize: vertical;
  min-height: 120px;
  font-family: inherit;
}

.btn-full {
  width: 100%;
}

Footer — HTML

<footer class="site-footer">
  <div class="footer-content">
    <p class="footer-brand">Alex Ionescu</p>
    <p class="footer-tagline">Frontend Developer</p>

    <div class="footer-social">
      <a href="https://github.com/alex" aria-label="GitHub" target="_blank" rel="noopener">
        GitHub
      </a>
      <a href="https://linkedin.com/in/alex" aria-label="LinkedIn" target="_blank" rel="noopener">
        LinkedIn
      </a>
      <a href="https://twitter.com/alex" aria-label="Twitter" target="_blank" rel="noopener">
        Twitter
      </a>
    </div>

    <p class="footer-copyright">
      © 2026 Alex Ionescu. Construit cu multă cafea.
    </p>
  </div>
</footer>

Footer — CSS

.site-footer {
  padding: 4rem var(--space-inline) 2rem;
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-section);
  text-align: center;
}

.footer-brand {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  margin-bottom: 0.25rem;
}

.footer-tagline {
  color: var(--color-text-soft);
  font-size: 0.9375rem;
  margin-bottom: 2rem;
}

.footer-social {
  display: flex;
  justify-content: center;
  gap: 1.5rem;
  margin-bottom: 2rem;
}

.footer-social a {
  color: var(--color-text-soft);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.9375rem;
  position: relative;
  transition: color 200ms;
}

.footer-social a::after {
  content: "";
  position: absolute;
  bottom: -4px;
  left: 0;
  width: 100%;
  height: 1px;
  background: var(--color-accent);
  transform: scaleX(0);
  transition: transform 200ms;
}

.footer-social a:hover {
  color: var(--color-accent);
}

.footer-social a:hover::after {
  transform: scaleX(1);
}

.footer-copyright {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  padding-top: 2rem;
  border-top: 1px solid var(--color-border);
  max-width: 600px;
  margin: 0 auto;
}

Polish — detaliile care contează

1. Smooth scroll între secțiuni

html {
  scroll-behavior: smooth;
  scroll-padding-top: 80px; /* offset pentru navbar sticky */
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto; /* respect pentru accesibilitate */
  }
}

Când apeși „Proiecte" din nav, pagina alunecă lin, nu sare. Offset-ul ține cont de navbar-ul sticky.

2. Text selection color

::selection {
  background: var(--color-accent);
  color: white;
}

Când cineva selectează text pe site-ul tău, apare cu culoarea de brand. Detaliu mic, impact mare.

3. Focus ring consistent

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
  border-radius: 2px;
}

Un stil consistent de focus peste tot — butoane, link-uri, inputs. Utilizatorii cu tastatura știu mereu unde sunt.

4. Link-urile externe cu iconiță

a[target="_blank"]:not(.btn):not(.nav-menu a)::after {
  content: " ↗";
  font-size: 0.85em;
  opacity: 0.7;
}

Toate link-urile care se deschid în tab nou primesc automat un indicator. Excludem butoanele (deja sunt clare).

5. Scroll-ul progres (reading bar)

@keyframes progressGrow {
  from { width: 0; }
  to { width: 100%; }
}

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: var(--color-accent);
  z-index: 100;
  width: 0;
}

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .scroll-progress {
      animation: progressGrow linear;
      animation-timeline: scroll();
    }
  }
}

O bară subțire sus care crește pe măsură ce scroll-ezi. Utilizatorii văd cât mai au de parcurs.

Checklist înainte să publici

Înainte să trimiți portofoliul oricui:

  • ✅ Verificat pe telefon, tabletă, desktop
  • ✅ Toate link-urile merg (inclusiv cele la proiecte)
  • ✅ Imaginile sunt optimizate (WebP, sub 200KB fiecare)
  • ✅ Formularul funcționează (testat cu date reale)
  • ✅ Favicon setat
  • ✅ Meta tag-uri (title, description, og:image) completate
  • ✅ Nume de fișiere fără spații sau diacritice
  • ✅ 404 page personalizată (dacă găzduiești static)
  • ✅ Testat cu Lighthouse (target: 95+ pe toate categoriile)
  • ✅ Citit tot textul pentru typos și erori
Testează pe un prieten

Ultimul test: dă link-ul unui prieten non-tehnic. Privește-l cum folosește site-ul timp de 2 minute.

Vei vedea instant:

  • Unde se blochează
  • Ce nu înțelege
  • Ce butoane nu sunt clare
  • Cât de greu încarcă

5 minute cu un utilizator real bat 5 ore de auto-review.

Recapitulare
  • Contact cu două coloane: info + formular
  • Contact methods ca link-uri tap-abile (mailto, tel)
  • Footer cu brand + social links + copyright
  • Polish: smooth scroll, selection color, focus ring, external link indicators
  • Reading progress bar pentru context
  • Checklist înainte de launch
  • Test final: un prieten non-tehnic

🎉 Ai terminat Modulul 10 — proiectul final!
Ai un portofoliu complet, production-ready. Urmează Modulul 11 — ultimul — despre cum să-l pui online.

Modulul 11 · Lecția 18 min citire

Deployment pe Netlify

Ai portofoliul pe calculator. Super. Dar până nu e online, e doar un fișier. Urmează 5 minute care-l pun pe internet, gratuit, cu HTTPS, la o adresă pe care o poți trimite oricui. Fără server, fără complicații.

Ce e Netlify

Netlify e un serviciu care hostuiește site-uri statice (HTML, CSS, JS) gratuit. Fără setup de server, fără bază de date. Îți iei un cont, încarci fișierele, primești un link.

Pentru portofoliul tău (sau orice site static), e exact ce-ți trebuie.

De ce Netlify în loc de altele

  • 100% gratuit pentru proiecte personale (până la anumite limite generoase)
  • HTTPS automat — site-ul tău are certificat SSL din prima secundă
  • CDN global — se încarcă rapid oriunde în lume
  • Drag-and-drop deploy — încarci folderul, gata
  • Deploy automat din GitHub (optional, pentru proiecte serioase)
  • Forms gratuite — până la 100 trimiteri/lună fără backend
  • Preview URL-uri pentru fiecare modificare

Pasul 1: Pregătește fișierele

Pune toate fișierele portofoliului într-un folder:

portofoliu/
├── index.html
├── styles.css
├── main.js
├── images/
│   ├── proj-1.webp
│   ├── proj-2.webp
│   └── proj-3.webp
├── favicon.ico
└── og-image.jpg

Important:

  • Fișierul principal trebuie să se cheme index.html
  • Fără spații sau diacritice în nume de fișiere
  • Link-urile în HTML trebuie să fie relative (href="styles.css", nu href="/Users/tu/.../styles.css")

Pasul 2: Creează cont Netlify

  1. Mergi la netlify.com
  2. Click „Sign up" (dreapta sus)
  3. Alege una dintre metodele: GitHub, GitLab, Email
  4. Pentru început, email e cel mai simplu
  5. Confirmă email-ul

Pasul 3: Deploy prin drag-and-drop

După login, ajungi pe dashboard. Cea mai simplă metodă de deploy:

  1. Scroll până găsești secțiunea „Sites"
  2. Click „Add new site" → „Deploy manually"
  3. Deschide folderul portofoliu/ pe calculatorul tău
  4. Selectează tot conținutul (Ctrl+A sau Cmd+A)
  5. Drag-and-drop în zona Netlify
  6. Așteptați 10-30 secunde

Gata. Site-ul tău e online.

✓ Site is live
URL: https://melodic-froyo-1a2b3c.netlify.app

URL-ul aleator

Netlify generează un URL aleator ca melodic-froyo-1a2b3c.netlify.app. Poți schimba partea de început:

  1. În dashboard, dai click pe site
  2. Settings → Site details → Change site name
  3. Alege ceva relevant: alex-ionescu.netlify.app

Pentru domeniu personal (ex. alexionescu.ro), treci la lecția 11-3.

Actualizări — cum schimbi fișierele

Când faci modificări la portofoliu, ai 2 opțiuni:

Metoda 1: Drag-and-drop din nou

  1. Dashboard → site-ul tău → tab „Deploys"
  2. „Trigger deploy" → „Deploy site"
  3. Sau drag-and-drop fișierele noi peste zona indicată

Metoda 2: Git (recomandată pentru proiecte serioase)

Pui proiectul pe GitHub, conectezi repo-ul la Netlify. Fiecare git push trimite site-ul automat. Se numește „continuous deployment" și e modul profesional de lucru.

Pentru asta, ai nevoie de Git — subiect pentru un curs separat. Pentru moment, drag-and-drop e perfect.

Formular de contact cu Netlify Forms

Dacă ai un formular de contact în portofoliu (din Modulul 9), îl poți face să trimită emailuri reale adăugând un atribut:

<form name="contact" netlify>
  ...
</form>

Atât. Netlify detectează automat formularul, colectează trimiterile, trimite email cu fiecare. 100/lună gratuit. Pentru portofoliu personal, mai mult decât suficient.

Ce primești gratis cu Netlify

  • HTTPS cu certificat SSL
  • 100 GB trafic/lună
  • 300 min build-uri/lună
  • Deploy nelimitat
  • Forms — 100 trimiteri/lună
  • Branch deploys (preview pentru fiecare versiune)
  • Analytics de bază

Pentru portofoliu personal, nu vei depăși niciodată limitele. Totul rămâne gratis.

Recapitulare
  • Netlify = host gratuit pentru site-uri statice
  • Fișierul principal trebuie să fie index.html
  • Deploy prin drag-and-drop — 30 secunde
  • HTTPS automat, CDN global, preview URL-uri
  • Poți schimba URL-ul în Settings → Site details
  • Pentru updates: drag-and-drop din nou sau conectează GitHub
  • Netlify Forms gratuit — 100 trimiteri/lună cu netlify pe form
Modulul 11 · Lecția 27 min citire

Deployment pe Vercel

Vercel e alternativa principală la Netlify. Aceeași idee — host static gratuit, deploy rapid. Câteva diferențe subtile. Te las să alegi pe care-ți place.

Netlify vs Vercel — diferențele reale

Netlify

  • Pionier în spațiul ăsta (primul din lista „JAMstack")
  • Forms native (100/lună gratuit)
  • Drag-and-drop simplu
  • Comunitate mare, multă documentație în română
  • Interfață puțin mai simplă pentru începători

Vercel

  • Făcut de echipa Next.js — cel mai bun pentru Next.js
  • Analytics mai bune și gratuite
  • Edge Functions rapide (dacă ajungi acolo)
  • Preview URL-uri excelente
  • Mai potrivit pentru proiecte React/Next.js

Pentru portofoliu simplu HTML/CSS: ambele sunt identice ca experiență. Dacă planifici să înveți React mai târziu, alege Vercel. Altfel, Netlify.

Pasul 1: Creează cont Vercel

  1. Mergi la vercel.com
  2. Click „Sign Up"
  3. Recomandare: folosește GitHub (integrare mai bună)
  4. Sau email, dacă nu ai GitHub încă

Pasul 2: Opțiunea rapidă — Vercel CLI

Vercel recomandă instalarea unui instrument de comandă. Pentru cine are Node.js:

# Instalare (o singură dată)
npm install -g vercel

# În folder-ul portofoliului
cd /Users/tu/portofoliu
vercel

# Răspunzi la câteva întrebări
# ✔ Set up and deploy? [Y/n] y
# ✔ Which scope? Your Name
# ✔ Link to existing project? [y/N] n
# ✔ What's your project's name? portofoliu
# ✔ In which directory is your code located? ./

# În 20-30 secunde, site-ul e live
# ✔ Preview: https://portofoliu-xyz.vercel.app

Pasul 3: Opțiunea fără terminal — GitHub

Dacă Node.js/terminalul te intimidează:

  1. Pui proiectul pe GitHub (poți folosi GitHub Desktop pentru UI prietenos)
  2. În Vercel dashboard, „Add New Project"
  3. Conectezi cu GitHub, selectezi repo-ul
  4. Vercel detectează că e site static, face deploy automat

Pasul 4: Upload direct (drag-and-drop)

Vercel suportă și drag-and-drop ca Netlify, dar mai puțin promovat:

  1. Dashboard → „Add New..." → „Project"
  2. Scroll jos, „Deploy a Static Site" (sau similar)
  3. Drag folder-ul

După deploy

Primești un URL ca portofoliu-xyz.vercel.app. Poți:

  • Schimba numele din Settings
  • Conecta domeniu custom
  • Vedea analytics (câte vizite, de unde)
  • Vedea deploy log-uri pentru debug
Care alegere pentru tine

Pentru portofoliul din cursul ăsta (HTML/CSS pur), am o recomandare clară:

  • Dacă vrei cel mai simplu start: Netlify cu drag-and-drop
  • Dacă planifici să înveți React/Next.js în viitor: Vercel (te obișnuiești cu ecosistemul)
  • Dacă ai deja GitHub: ambele sunt la fel, alege orice

Ambele sunt gratuite, ambele sunt producție-ready, ambele au comunități mari. Nu e o decizie reversibilă — poți muta site-ul în 5 minute.

Ce primești gratis cu Vercel

  • HTTPS cu SSL
  • 100 GB trafic/lună
  • Build-uri practic nelimitate (6000 min/lună)
  • Deploy nelimitat
  • Analytics excelent (diferit de Netlify — mai detaliat)
  • Preview pentru fiecare commit/push

Ce NU ai în Vercel (față de Netlify)

  • Nu are Forms native — trebuie serviciu separat (Formspree, Basin)
  • Interfață puțin mai tehnică pentru începători
Recapitulare
  • Vercel = alternativă la Netlify, funcționalități similare
  • Pentru portofoliu simplu: ambele la fel
  • Vercel mai bun dacă vei folosi Next.js în viitor
  • Netlify mai simplu pentru începători absoluți
  • Deploy prin CLI, GitHub, sau drag-and-drop
  • Ambele gratuite pentru uz personal
Modulul 11 · Lecția 39 min citire

Domeniu custom și email

alex-ionescu.netlify.app merge, dar alexionescu.ro convinge mai mult. Un domeniu custom costă ~40 lei/an și durează 15 minute să-l configurezi. Plus îți poți face email alex@alexionescu.ro — arată profesional față de alexionescu.dev@gmail.com.

De ce merită un domeniu custom

  • Credibilitate — clienții serioși se așteaptă la un domeniu real
  • Memorabil — numele tău punct ceva < subdomenii aleatoare
  • Email profesional@domeniultau.ro bate Gmail-ul
  • Portabil — dacă schimbi host-ul, domeniul rămâne
  • Ieftin — sub 50 lei/an pentru majoritatea TLD-urilor

Ce TLD să alegi

TLD = partea de după punct (.com, .ro, .dev, etc.). Recomandări:

  • .ro — ideal pentru români care lucrează cu clienți români. ~40-60 lei/an
  • .com — universal, recunoscut internațional. ~50-80 lei/an
  • .dev — trendy pentru dev-eri. ~70-100 lei/an
  • .io — popular în tech. ~150-200 lei/an
  • .me — personal (alex.me). ~80-150 lei/an

Unde cumperi

Registraruri populari, în ordinea preferinței:

1. Namecheap (internațional)

  • Prețuri competitive
  • Interfață simplă
  • WHOIS privacy gratuit
  • Suport 24/7 în chat

2. Cloudflare Registrar (cel mai ieftin)

  • Preț la cost — fără markup
  • DNS rapid, securitate bună
  • Dar: cerere să ai cont Cloudflare
  • Doar anumite TLD-uri (nu și .ro)

3. Rotld / Registrari români (pentru .ro)

  • Pentru .ro, multe firme vând (Hostico, Romarg, GoDaddy, etc.)
  • Preț similar, diferențe mici
  • Atenție la reînnoire — unele firme dublează prețul în al doilea an

4. host-profesional.com (recomandarea mea pentru români)

Pentru cineva care e la început și lucrează cu clienți români, recomand host-profesional.com. Sunt transparent — e serviciul meu (al lui Alin, autorul cursului). Dar îl recomand fiindcă:

  • Prețuri corecte la .ro și .com, fără surprize la reînnoire
  • Suport în română, rapid (nu linie call center din Filipine)
  • Hosting pentru WordPress inclus dacă vrei să migrezi de pe Netlify mai târziu
  • Email profesional pe domeniu inclus în majoritatea planurilor
  • Experiență directă cu nevoile dev-erilor români

Dacă te încurci cu setările DNS, oamenii de la host-profesional.com le fac pentru tine — trimiți un ticket cu „vreau să conectez domeniul la Netlify" și te ajută.

Cumpărarea — pas cu pas (Namecheap exemplu)

  1. Mergi la namecheap.com
  2. Caută numele tău: alexionescu
  3. Site-ul arată TLD-urile disponibile cu prețuri
  4. Alege unul, „Add to Cart"
  5. Checkout cu card
  6. În 1-2 minute, domeniul e al tău

Conectarea domeniului la Netlify

Acum că ai alexionescu.ro, îl legi la site-ul tău Netlify:

Pasul 1: Netlify dashboard

  1. Dashboard → site-ul tău → „Domain settings"
  2. „Add custom domain" → introduci alexionescu.ro
  3. Verifică că e al tău → „Add domain"

Pasul 2: Setările DNS la registrar

La registrar (ex. Namecheap), găsești domeniul și accesezi DNS settings. Adaugi 2 înregistrări:

Type   | Host | Value                   | TTL
-------|------|-------------------------|------
A      | @    | 75.2.60.5              | Auto
CNAME  | www  | alexionescu.netlify.app| Auto

(Valorile exacte le iei din Netlify — ei îți arată ce să pui.)

Pasul 3: Așteaptă

DNS-ul se propagă în câteva minute până la 24 ore. De obicei, 15-30 minute. Apoi alexionescu.ro arată site-ul tău.

Pasul 4: HTTPS automat

Netlify detectează domeniul nou și generează certificat SSL în câteva minute. alexionescu.ro devine https://alexionescu.ro automat.

Email profesional

Cu domeniul alexionescu.ro, poți avea contact@alexionescu.ro. Două opțiuni:

Opțiunea 1: Google Workspace (recomandat)

  • 30 lei/lună — pentru Gmail pe domeniul tău
  • Interfața Gmail familiară
  • 15 GB storage
  • Integrare cu Calendar, Drive, Docs
  • Cel mai simplu și fiabil

Opțiunea 2: Zoho Mail (gratuit!)

  • Complet gratuit pentru un user
  • Până la 5 adrese email
  • 5 GB storage
  • Interfață ok, nu la fel de bună ca Gmail
  • Ideal pentru start

Opțiunea 3: Email forwarding (cel mai simplu)

Unii registrari (Namecheap, Cloudflare) oferă forwarding gratuit:

  • contact@alexionescu.roalexionescu.dev@gmail.com
  • Oamenii scriu la adresa profesională
  • Tu primești în Gmail-ul personal
  • Nu poți trimite din contact@alexionescu.ro — doar primești
Recomandarea pentru început

Pentru cineva care abia iese din curs și face primul portofoliu, recomand:

  1. Cumpără .ro la host-profesional.com — 40-60 lei, suport în română
  2. Folosește email forwarding gratuit sau planul cu email inclus — suficient la început
  3. Mai târziu, upgrade la Google Workspace când ai primii clienți

Total cost: ~40 lei/an pentru domeniu + host gratuit (Netlify) sau hosting inclus la host-profesional.com. Sub 5 euro/an pentru un portofoliu complet profesional.

Recapitulare
  • Domeniu custom = credibilitate + memorabil + portabil
  • .ro pentru clienți români (40-60 lei/an), .com pentru internațional
  • Cumperi la Namecheap, Cloudflare, Hostico
  • Conectezi la Netlify prin 2 înregistrări DNS (A + CNAME)
  • HTTPS automat, propagare în 15-30 minute
  • Email: Google Workspace (30 lei/lună), Zoho (gratuit), sau forwarding
Modulul 11 · Lecția 410 min citire

Drumul mai departe

Ai ajuns la capăt. 76 de lecții, 11 module, un portofoliu complet. Știi HTML, CSS modern, layouts, animații, validare, responsive, deployment. Ce urmează? Depinde unde vrei să ajungi. Hai să-ți dau o hartă.

Ce știi acum

Să facem inventarul. Știi:

  • HTML semantic complet — tag-uri, forms, accesibilitate, SEO
  • CSS de la zero la avansat — selectori, box model, flex, grid
  • Responsive design — mobile-first, clamp, container queries
  • Animații moderne — transitions, keyframes, scroll animations
  • Pattern-uri profesionale — design tokens, reset, component-based CSS
  • Accessibility — focus states, prefers-reduced-motion, semantic HTML
  • Deployment — de la local la URL public, cu domeniu propriu

Ești la nivel junior frontend solid. Nu începător absolut, nu senior — undeva între. Majoritatea job-urilor entry-level cer exact aceste skill-uri.

Ce NU știi încă

Ca să fii onest cu tine, iată ce-ți lipsește:

  • JavaScript — ai văzut câteva exemple, nu îl cunoști
  • Git și GitHub — esențial pentru orice job
  • Un framework (React, Vue, Svelte) — standardul industrial
  • TypeScript — 80% din proiecte noi îl folosesc
  • Backend de bază — să înțelegi cum funcționează API-urile

Sună mult. E. Dar ai deja fundația — fără asta, restul n-ar fi avut sens.

Drumul recomandat — următoarele 6-12 luni

Pasul 1: Git & GitHub (1 săptămână)

Înainte de orice altceva, învață Git. Fără Git:

  • Nu poți colabora cu alți dev-eri
  • Nu poți contribui la proiecte open-source
  • Nu poți folosi deployments moderne
  • Nu poți aplica pentru job-uri tech

Resurse:

  • learngitbranching.com — interactiv, vizual, excelent
  • Freecodecamp Git tutorial (YouTube)
  • Cartea „Pro Git" — gratuită online, capitol 1-3 suficient

Pasul 2: JavaScript fundamentele (1-2 luni)

JavaScript e limbajul web-ului. Tot ce va urma — React, Vue, Node — se bazează pe el.

Ce să înveți:

  • Variabile, funcții, condiții, loops
  • DOM manipulation — querySelector, addEventListener
  • Fetch API — să iei date de la un server
  • Promises, async/await
  • Array methods — map, filter, reduce
  • ES6+ features — destructuring, spread, arrow functions

Resurse:

  • javascript.info — cea mai bună documentație gratuită
  • MDN Web Docs — referință completă
  • Wes Bos — JavaScript 30 (30 proiecte)

Pasul 3: Un framework (2-3 luni)

După JavaScript solid, alege un framework. Recomandarea mea: React.

De ce React:

  • Cel mai cerut pe piața job-urilor
  • Comunitate enormă, multe resurse
  • Ecosistem vast (Next.js, Remix, frameworks specializate)
  • Transferabil la React Native (mobile)

Alternative:

  • Vue — mai simplu, popular în Asia și Europa
  • Svelte — modern, eficient, nișă
  • Angular — pentru enterprise, corporate

Pasul 4: TypeScript (1 lună, paralel)

TypeScript = JavaScript cu tipuri. Începe ușor. În 6 luni, e standard. Învață-l odată cu React.

Pasul 5: Primul job sau client (cât ia)

Nu aștepta să „știi tot" înainte să aplici. Nimeni nu știe tot.

După Pas 4, pregătit să aplici pentru:

  • Junior frontend developer
  • Freelance — landing pages, site-uri mici
  • Internship-uri

Resurse pentru învățare continuă

Blog-uri/newsletter-uri de urmărit

  • CSS-Tricks — totul despre CSS, nivel avansat
  • web.dev — Google, performanță și best practices
  • Josh Comeau — bloguri și cursuri CSS (plătite dar excelente)
  • Kent C. Dodds — React, testing
  • Lea Verou — CSS avansat

YouTube

  • Fireship — concepte explicate rapid, 100 secunde
  • Web Dev Simplified — tutoriale clare
  • Theo (T3.gg) — news și opinii în tech
  • Kevin Powell — cel mai bun canal CSS

Comunități

  • Dev.to — blog-uri de la dev-eri, curba de intrare scăzută
  • r/webdev, r/Frontend — Reddit, util pentru discuții
  • Discord/Slack-uri — Hashnode, CSS Tricks, frameworks
  • Twitter — urmărește dev-eri relevanți, învață zi de zi

Cursuri platite care merită

  • Josh Comeau — CSS for JS Developers — cel mai bun curs CSS avansat
  • Frontend Masters — subscripție, toate subiectele
  • Kent C. Dodds — Epic React — cel mai complet pentru React

Cursuri viitoare din Școala de WordPress

Dacă ți-a plăcut stilul ăsta de predare, urmează:

  • JavaScript pentru dev-eri frontend — 10 module, de la zero la React-ready
  • WordPress development modern — teme și plugin-uri profesionale
  • WooCommerce dev — magazin-uri online complete
  • React pentru practicanți — aplicații reale, nu tutoriale

Pentru hosting și domenii

Când îți lansezi primele proiecte (portofoliu, site-uri de client), îți recomand host-profesional.com. E serviciul meu, așa că sunt parte interesată — dar îl recomand deoarece știu direct de ce au nevoie dev-erii români la început: prețuri corecte, suport rapid în română, hosting care scalează de la portofoliu la magazin WooCommerce.

Bonus: cursul ăsta e făcut cu ❤️ de la host-profesional.com — dacă vrei să ne susții, domeniul tău următor făcut de aici e cel mai bun mulțumesc.

Sfatul final

Construiește proiecte, nu doar lecții. Orice curs, oricât de bun, e doar teorie până îl aplici.

Pentru fiecare modul nou pe care-l înveți, fă un mic proiect. Pune-l pe GitHub. Pune link în portofoliu. În 6 luni vei avea 10-15 proiecte reale — mult mai valoros decât orice certificat.

Cel mai important sfat

Nu te opri aici. Majoritatea oamenilor care termină un curs de frontend nu ajung niciodată dev-eri reali. De ce? Se opresc. Cred că „au terminat". Nu e terminat nimic.

Dev-ul e o meserie în care înveți toată viața. Tehnologia se schimbă. Framework-uri noi apar. Best practices evoluează. Dar fundația pe care o ai acum — HTML, CSS, design responsive, thinking semantic — rămâne valabilă mereu.

Construiește. Publică. Aplică pentru job-uri chiar dacă nu te simți pregătit. Cel mai bun moment să începi a fost acum 5 ani. Al doilea cel mai bun e astăzi.

Recapitulare finală
  • Ești la nivel junior frontend solid
  • Următorul pas: Git (1 săpt) → JavaScript (1-2 luni) → React (2-3 luni) → TypeScript
  • Resurse: javascript.info, MDN, Josh Comeau, Kevin Powell
  • Comunități: Dev.to, Reddit, Discord
  • Construiește proiecte constant — mai valoros decât certificate
  • Nu aștepta să „știi tot" — aplică pentru job-uri cât de curând
  • Învățarea nu se oprește aici. Niciodată.

🎓 Ai terminat cursul complet HTML & CSS!

76 de lecții, 11 module, sute de exemple, un portofoliu real, totul în română.

Mulțumesc că ai avut încredere în acest curs.
Abia aștept să văd ce vei construi.

— Alin, Școala de WordPress

ComunitateRecenzii și feedback

Ce spun cursanții

Cursul ăsta crește prin feedback-ul vostru. Pe măsură ce vor veni recenzii și impresii, le voi adăuga aici. Iar dacă tu ai parcurs cursul — pozitiv sau cu sugestii — te invit să-mi scrii.

Recenzii recente

★★★★★

Cel mai bun curs de HTML/CSS în română pe care l-am parcurs. Explicațiile sunt clare, exemplele sunt practice și am terminat cu un portofoliu pe care chiar îl pot arăta.

— Maria, junior frontend

★★★★★

Mi-a plăcut că nu e doar teorie. Fiecare modul are exerciții și proiecte. Am construit un dashboard complet din zero după Modulul 5.

— Andrei, dev junior

★★★★★

Recomand cu încredere. Am încercat alte cursuri pe Udemy, dar acesta e mult mai bine structurat și totul e în română. Container queries, :has(), animation-timeline — explicate cu exemple clare.

— Cristian, web designer

Lasă-mi părerea ta

Cu un click se deschide email-ul cu un mesaj pre-completat. Tu doar îl personalizezi și-l trimiți.

Lasă o recenzie

Sau scrie-mi direct la contact@creare-site-web.com

💡 Cu permisiunea ta, recenzia poate apărea aici sus, ca să ajute alți cursanți să decidă.