···955955})
956956```
957957:::
958958+959959+## Custom Locators <Version>3.2.0</Version> <Badge type="danger">advanced</Badge> {#custom-locators}
960960+961961+You can extend built-in locators API by defining an object of locator factories. These methods will exist as methods on the `page` object and any created locator.
962962+963963+These locators can be useful if built-in locators are not enough. For example, when you use a custom framework for your UI.
964964+965965+The locator factory needs to return a selector string or a locator itself.
966966+967967+::: tip
968968+The selector syntax is identical to Playwright locators. Please, read [their guide](https://playwright.dev/docs/other-locators) to better understand how to work with them.
969969+:::
970970+971971+```ts
972972+import { locators } from '@vitest/browser/context'
973973+974974+locators.extend({
975975+ getByArticleTitle(title) {
976976+ return `[data-title="${title}"]`
977977+ },
978978+ getByArticleCommentsCount(count) {
979979+ return `.comments :text("${count} comments")`
980980+ },
981981+ async previewComments() {
982982+ // you have access to the current locator via "this"
983983+ // beware that if the method was called on `page`, `this` will be `page`,
984984+ // not the locator!
985985+ if (this !== page) {
986986+ await this.click()
987987+ }
988988+ // ...
989989+ }
990990+})
991991+992992+// if you are using typescript, you can extend LocatorSelectors interface
993993+// to have the autocompletion in locators.extend, page.* and locator.* methods
994994+declare module '@vitest/browser/context' {
995995+ interface LocatorSelectors {
996996+ // if the custom method returns a string, it will be converted into a locator
997997+ // if it returns anything else, then it will be returned as usual
998998+ getByArticleTitle(title: string): Locator
999999+ getByArticleCommentsCount(count: number): Locator
10001000+10011001+ // Vitest will return a promise and won't try to convert it into a locator
10021002+ previewComments(this: Locator): Promise<void>
10031003+ }
10041004+}
10051005+```
10061006+10071007+If the method is called on the global `page` object, then selector will be applied to the whole page. In the example bellow, `getByArticleTitle` will find all elements with an attribute `data-title` with the value of `title`. However, if the method is called on the locator, then it will be scoped to that locator.
10081008+10091009+```html
10101010+<article data-title="Hello, World!">
10111011+ Hello, World!
10121012+ <button id="comments">2 comments</button>
10131013+</article>
10141014+10151015+<article data-title="Hello, Vitest!">
10161016+ Hello, Vitest!
10171017+ <button id="comments">0 comments</button>
10181018+</article>
10191019+```
10201020+10211021+```ts
10221022+const articles = page.getByRole('article')
10231023+const worldArticle = page.getByArticleTitle('Hello, World!') // ✅
10241024+const commentsElement = worldArticle.getByArticleCommentsCount(2) // ✅
10251025+const wrongCommentsElement = worldArticle.getByArticleCommentsCount(0) // ❌
10261026+const wrongElement = page.getByArticleTitle('No Article!') // ❌
10271027+10281028+await commentsElement.previewComments() // ✅
10291029+await wrongCommentsElement.previewComments() // ❌
10301030+```