Playwright Page Object Model in TypeScript (2026 Guide)
Step-by-step guide to Playwright Page Object Model in TypeScript with real examples. Base page, page classes, fixtures, component POM, and best practices for 2026.

The Page Object Model (POM) is the most important pattern for any non-trivial Playwright project. This guide walks you through the full TypeScript POM pattern with real examples — base page, page classes, component POM, and fixtures. If you're new to Playwright, start with our Playwright complete guide, then come back here to design a framework that scales.
What is the Page Object Model?
POM is a design pattern where each page (or component) is a class with locators as properties and actions as methods. Tests interact with page objects, never raw locators.
Benefits:
- One place to update locators when the UI changes
- Tests read like user stories, not scripts
- Parallel authoring of tests and locators
- Easier code review
For the broader Playwright context, see our Playwright Complete Guide.
Project Structure
playwright-pom/
├── tests/
│ ├── login.spec.ts
│ └── checkout.spec.ts
├── pages/
│ ├── base-page.ts
│ ├── login-page.ts
│ ├── dashboard-page.ts
│ └── checkout-page.ts
├── components/
│ ├── header.ts
│ └── footer.ts
├── fixtures/
│ └── test-fixtures.ts
├── playwright.config.ts
└── package.jsonThe Base Page
Every page object extends a base class with shared behavior:
// pages/base-page.ts
import { Page, Locator, expect } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
abstract get url(): string;
async goto(): Promise<void> {
await this.page.goto(this.url);
await this.waitForLoaded();
}
abstract waitForLoaded(): Promise<void>;
async getTitle(): Promise<string> {
return await this.page.title();
}
}The Login Page
// pages/login-page.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './base-page';
import { DashboardPage } from './dashboard-page';
export class LoginPage extends BasePage {
get url(): string {
return '/login';
}
private get emailField(): Locator {
return this.page.getByTestId('email');
}
private get passwordField(): Locator {
return this.page.getByTestId('password');
}
private get submitButton(): Locator {
return this.page.getByTestId('submit');
}
private get errorMessage(): Locator {
return this.page.getByTestId('error-message');
}
async waitForLoaded(): Promise<void> {
await this.emailField.waitFor({ state: 'visible' });
}
async loginAs(email: string, password: string): Promise<DashboardPage> {
await this.emailField.fill(email);
await this.passwordField.fill(password);
await this.submitButton.click();
return new DashboardPage(this.page);
}
async getErrorText(): Promise<string> {
return (await this.errorMessage.textContent()) ?? '';
}
}The Dashboard Page
// pages/dashboard-page.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './base-page';
export class DashboardPage extends BasePage {
get url(): string {
return '/dashboard';
}
private get heading(): Locator {
return this.page.getByRole('heading', { name: /welcome/i });
}
private get logoutButton(): Locator {
return this.page.getByTestId('logout');
}
async waitForLoaded(): Promise<void> {
await this.page.waitForURL(/\/dashboard$/);
}
async getWelcomeMessage(): Promise<string> {
return (await this.heading.textContent()) ?? '';
}
async logout(): Promise<void> {
await this.logoutButton.click();
}
}The Test
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test.describe('Login flow', () => {
test('successful login redirects to dashboard', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
const dashboard = await login.loginAs('admin@example.com', 'Sup3rSecret!');
await dashboard.waitForLoaded();
expect(await dashboard.getWelcomeMessage()).toContain('Welcome');
});
test('invalid credentials shows error', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.loginAs('admin@example.com', 'wrong-password');
expect(await login.getErrorText()).toContain('Invalid credentials');
});
});The test reads like a user story — much easier to maintain than raw locators.
Component POM (Recommended for 2026)
Modern apps have reusable components (header, footer, modal). Component POM is the same pattern, scoped to a component:
// components/header.ts
import { Page, Locator } from '@playwright/test';
export class Header {
constructor(private page: Page) {}
private get logo(): Locator {
return this.page.getByTestId('logo');
}
private get navLinks(): Locator {
return this.page.getByRole('navigation').getByRole('link');
}
async clickLogo(): Promise<void> {
await this.logo.click();
}
async clickNavLink(name: string): Promise<void> {
await this.navLinks.filter({ hasText: name }).click();
}
async getNavLinkCount(): Promise<number> {
return await this.navLinks.count();
}
}Use it in a page:
import { Header } from '../components/header';
export class DashboardPage extends BasePage {
readonly header: Header;
constructor(page: Page) {
super(page);
this.header = new Header(page);
}
async clickNavLink(name: string): Promise<void> {
await this.header.clickNavLink(name);
}
}Using Fixtures for Cleaner Tests
Playwright fixtures let you inject page objects into tests:
// fixtures/test-fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { DashboardPage } from '../pages/dashboard-page';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';Now tests are even cleaner:
// tests/login.spec.ts
import { test, expect } from '../fixtures/test-fixtures';
test('successful login', async ({ loginPage, page }) => {
await loginPage.goto();
const dashboard = await loginPage.loginAs('admin@example.com', 'Sup3rSecret!');
expect(await dashboard.getWelcomeMessage()).toContain('Welcome');
});The official Playwright fixtures docs cover advanced patterns like worker-scoped fixtures and auto-fixtures.
Best Practices for 2026
- One page class per logical page — don't combine multiple pages.
- Use
getByRole,getByTestId,getByLabel— avoid CSS/XPath. - Keep methods user-focused —
loginAs, notfillEmail + fillPassword + clickSubmit. - Return new page objects from action methods — for navigation flows.
- Use
waitForLoaded()after navigation — auto-wait isn't enough for complex apps. - Component POM for shared UI — header, footer, modal.
- Use fixtures for cleaner tests — extend
testwith your page objects. - Avoid hard-coded URLs in tests — use
goto()from the page object.
Anti-Patterns to Avoid
❌ Locators in tests
// BAD
test('login', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByTestId('email').fill('admin@example.com');
await page.getByTestId('submit').click();
});✅ POM everywhere
// GOOD
test('login', async ({ loginPage, page }) => {
await loginPage.goto();
await loginPage.loginAs('admin@example.com', 'Sup3rSecret!');
});❌ Methods that return locators
// BAD — tests can use the locator and bypass POM
get emailField(): Locator { return this.page.getByTestId('email'); }✅ Methods that perform actions
// GOOD — tests must use the action
async enterEmail(email: string): Promise<void> {
await this.emailField.fill(email);
}Keep Learning
Once your POM is solid, level up your suite with a complete framework setup in our Playwright framework with TypeScript guide, then prep for interviews using Playwright interview questions and our AI mock interview. If you're choosing between tools, compare options in Playwright vs Selenium.
Frequently asked questions
1.What is the Page Object Model in Playwright?
2.Should I use POM in Playwright?
3.What's the difference between POM and component POM?
4.How do I share page objects across tests?
5.Should I expose locators from page objects?
Practice these questions
Drill 200+ Playwright questions with senior-SDET sample answers — locators, auto-wait, fixtures, parallelism and trace viewer.
Was this article helpful?
Keep building your QA edge
Pillar guides- Selenium PillarSelenium WebDriver guide300 Selenium WebDriver Q&A — locators, waits, frameworks.
- Playwright Installation Guidethe full setup walkthroughInstall Playwright the right way — Node, browsers, VS Code, first test.
- XPath & CSS Selector GeneratorXPath and CSS selector generatorInteractive locator generator for Playwright, Selenium and Cypress — with Page Object export.
- Automation QA Engineer RoleAutomation QA Engineer career guideAutomation QA Engineer job scope, tools, salary, and hiring pipeline.
- QA Skills Hubstructured learning tracks for testersStructured skill tracks — Selenium, Playwright, Cypress, API, JMeter, SQL, Java, Python for testers.
Practice these questions live
Rehearse with an AI QA interviewer that scores your answers in real time.
Continue reading

Playwright Locator Best Practices (2026) — The Only Guide You Need
11 min read
How to Migrate a Postman Collection to Playwright API Tests (2026 Guide)
12 min read
Why Every QA Engineer Must Master CI/CD Pipelines in 2026 (Or Risk Obsolescence)
12 min readRelated concepts, tools & standards around Automation Testing
A quick reference of the people, companies, frameworks and technologies most often mentioned alongside Automation Testing in real QA teams — useful when you're mapping a learning path, preparing for interviews, or scoping a new project.
Join the QA Community
Connect with fellow testers, share job leads, and get career advice.
Discussion
Ask a question, share your experience, or correct us. Be kind — real people are reading.