ZetCode

C# Playwright

last modified October 18, 2026

In this article we show how to automate browsers in C# using the Microsoft Playwright library.

Playwright

Playwright is a cross-browser automation library created by Microsoft. It supports all modern rendering engines including Chromium, WebKit, and Firefox. It is used for end-to-end testing of web applications and for scraping pages that are generated with JavaScript.

Playwright can be used in Node, Python, .NET and Java.

By default, Playwright runs the browser in headless mode, which works without the UI. This is great for scripting and for running tests on servers.

$ dotnet new console -o PlaywrightEx
$ cd PlaywrightEx
$ dotnet add package Microsoft.Playwright
$ dotnet build

We create a console project and add the Playwright package. The build creates the playwright.ps1 script in the output directory.

$ pwsh bin/Debug/net10.0/playwright.ps1 install chromium

Playwright needs its own builds of the browsers. The script downloads them; without an argument, it downloads all supported browsers. The script requires PowerShell; if the pwsh command is not available, PowerShell can be installed with dotnet tool install --global PowerShell. On Linux, playwright.ps1 install-deps installs the system libraries the browsers need.

C# Playwright get title

In the first example, we get the title of a web page.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");

var title = await page.TitleAsync();
Console.WriteLine(title);

The example opens a web page in a browser and retrieves its title.

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

We set up Playwright and launch the Chromium browser. Disposing the objects closes the browser.

var page = await browser.NewPageAsync();

The NewPageAsync method creates a new page in a new browser context. A browser context is like an incognito window: it has its own cookies and storage.

await page.GotoAsync("https://example.com");

The GotoAsync method navigates to the URL and waits until the page is loaded.

var title = await page.TitleAsync();
Console.WriteLine(title);

The page title is retrieved with TitleAsync.

$ dotnet run
Example Domain

C# Playwright headers

The next example adds headers to the requests of the page.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();

var headers = new Dictionary<string, string>
{
    ["User-Agent"] = "C# program",
    ["X-Request-Source"] = "zetcode"
};

await page.SetExtraHTTPHeadersAsync(headers);

var resp = await page.GotoAsync("https://httpbin.org/headers");
Console.WriteLine($"Status: {resp?.Status}");

var json = await resp!.JsonAsync();
var received = json?.GetProperty("headers");

Console.WriteLine(received?.GetProperty("User-Agent"));
Console.WriteLine(received?.GetProperty("X-Request-Source"));

We set the User-Agent header and a custom header. The httpbin.org/headers endpoint returns the headers it received as JSON.

await page.SetExtraHTTPHeadersAsync(headers);

The headers are added to every request that the page makes.

var resp = await page.GotoAsync("https://httpbin.org/headers");
Console.WriteLine($"Status: {resp?.Status}");

var json = await resp!.JsonAsync();
var received = json?.GetProperty("headers");

The GotoAsync method returns the response of the main resource. The JsonAsync method parses the body into a JsonElement.

$ dotnet run
Status: 200
C# program
zetcode

C# Playwright create screenshot

The next example creates screenshots of a web page.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync(new() { ViewportSize = new() { Width = 1280, Height = 800 } });

await page.GotoAsync("https://quotes.toscrape.com");

await page.ScreenshotAsync(new() { Path = "screenshot.png" });
await page.ScreenshotAsync(new() { Path = "fullpage.png", FullPage = true });

Console.WriteLine("screenshots saved");

A screenshot is created with ScreenshotAsync. We set the size of the viewport when creating the page. The first screenshot contains the visible part of the page; with the FullPage option, the screenshot contains the whole scrollable page.

$ dotnet run
screenshots saved

To watch the browser at work, launch it with new() { Headless = false }; this requires a graphical desktop.

C# Playwright locators

Locators are the recommended way to find elements in Playwright. A locator describes how to find an element; the element is looked up each time an action is performed, and Playwright automatically waits until the element is available. Locators can use CSS selectors or find elements by their role, text, or label.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com");

ILocator quotes = page.Locator("div.quote");
Console.WriteLine($"{await quotes.CountAsync()} quotes on the page");

for (int i = 0; i < 3; i++)
{
    ILocator quote = quotes.Nth(i);

    string author = await quote.Locator("small.author").InnerTextAsync();
    IReadOnlyList<string> tags = await quote.Locator("a.tag").AllInnerTextsAsync();

    Console.WriteLine($"{author}: {string.Join(", ", tags)}");
}

string first = await quotes.First.Locator("span.text").InnerTextAsync();
Console.WriteLine(first);

We extract quotes from the quotes.toscrape.com website, which is designed for practising web scraping.

ILocator quotes = page.Locator("div.quote");
Console.WriteLine($"{await quotes.CountAsync()} quotes on the page");

The locator matches all div elements with the quote class. The CountAsync method returns the number of matching elements.

ILocator quote = quotes.Nth(i);

string author = await quote.Locator("small.author").InnerTextAsync();
IReadOnlyList<string> tags = await quote.Locator("a.tag").AllInnerTextsAsync();

The Nth method selects one of the matches. Locators can be chained; the inner locators search only inside the quote. The AllInnerTextsAsync method returns the texts of all matching elements.

$ dotnet run
10 quotes on the page
Albert Einstein: change, deep-thoughts, thinking, world
J.K. Rowling: abilities, choices
Albert Einstein: inspirational, life, live, miracle, miracles
“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”

C# Playwright click on element

The ClickAsync method clicks an element. In the following example, we go through the pages of the quotes website by clicking the Next link.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com");

for (int i = 0; i < 3; i++)
{
    await page.GetByRole(AriaRole.Link, new() { Name = "Next" }).ClickAsync();
    await page.WaitForURLAsync($"**/page/{i + 2}/");

    string author = await page.Locator("small.author").First.InnerTextAsync();
    Console.WriteLine($"{page.Url} - first author: {author}");
}

We click the link three times and print the first author of each page.

await page.GetByRole(AriaRole.Link, new() { Name = "Next" }).ClickAsync();
await page.WaitForURLAsync($"**/page/{i + 2}/");

The GetByRole method finds the link by its accessible role and name, the way a user perceives the page. After the click, we wait until the browser navigates to the next page. The URL pattern is a glob, where ** matches any characters.

$ dotnet run
https://quotes.toscrape.com/page/2/ - first author: Marilyn Monroe
https://quotes.toscrape.com/page/3/ - first author: Pablo Neruda
https://quotes.toscrape.com/page/4/ - first author: Dr. Seuss

C# Playwright fill form

Playwright can fill in and submit forms. The login form of the quotes website accepts any user name and password.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();
await page.GotoAsync("https://quotes.toscrape.com/login");

await page.Locator("#username").FillAsync("john");
await page.Locator("#password").FillAsync("secret");
await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();

await page.GetByRole(AriaRole.Link, new() { Name = "Logout" }).WaitForAsync();

Console.WriteLine(page.Url);
Console.WriteLine("logged in");

We log in to the website.

await page.Locator("#username").FillAsync("john");
await page.Locator("#password").FillAsync("secret");
await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();

The FillAsync method types the text into the input fields, which we find by their ids. Then we click the Login button.

await page.GetByRole(AriaRole.Link, new() { Name = "Logout" }).WaitForAsync();

After a successful login, the page shows a Logout link. We wait for it to appear.

$ dotnet run
https://quotes.toscrape.com/
logged in

C# Playwright monitor requests and responses

The Request event is raised when a page issues a request. The Response event is raised when a response is received for a request.

Program.cs
using Microsoft.Playwright;

using var pw = await Playwright.CreateAsync();
await using var browser = await pw.Chromium.LaunchAsync();

var page = await browser.NewPageAsync();

page.Request += (_, req) => Console.WriteLine($">> {req.Method} {req.Url}");
page.Response += (_, resp) => Console.WriteLine($"<< {resp.Status} {resp.Url}");

await page.GotoAsync("https://quotes.toscrape.com");

We log a message for each request and response. Besides the page itself, the browser loads its stylesheets and fonts.

$ dotnet run
>> GET https://quotes.toscrape.com/
<< 200 https://quotes.toscrape.com/
>> GET https://quotes.toscrape.com/static/bootstrap.min.css
>> GET https://quotes.toscrape.com/static/main.css
<< 200 https://quotes.toscrape.com/static/main.css
<< 200 https://quotes.toscrape.com/static/bootstrap.min.css
>> GET https://fonts.googleapis.com/css?family=Raleway:400,700
<< 200 https://fonts.googleapis.com/css?family=Raleway:400,700

The requests are made in parallel, so the responses can arrive in a different order.

Source

Playwright documentation

In this article we have used the Playwright library to automate browsers in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.