Câu trả lời ngắn: để kéo được cửa sổ bằng giao diện tùy biến, bạn cần bật window-controls-overlay trong manifest, cài PWA trên máy tính, đặt window-drag: move cho vùng trống và đặt window-drag: none trực tiếp lên nút, ô nhập liệu cùng các phần tử tương tác. Nếu mở ứng dụng trong tab thông thường, thao tác kéo cửa sổ sẽ không hoạt động.
Trong phạm vi Chrome 152, window-drag là thuộc tính CSS dùng để đánh dấu vùng có thể di chuyển cửa sổ của ứng dụng web desktop. Cơ chế này không biến một trang web trong tab thành cửa sổ hệ điều hành; nó cần PWA đã cài và chế độ hiển thị hỗ trợ Window Controls Overlay (theo developer.chrome.com). Phần dưới dùng một ứng dụng ghi chú minh họa. Hãy thay tên ứng dụng, đường dẫn biểu tượng và nội dung theo dự án của bạn.
Chuẩn bị đúng môi trường trước khi viết CSS
Ví dụ này giả định bạn đã có một ứng dụng web có thể chạy qua HTTPS hoặc localhost, có tệp manifest và các biểu tượng ứng dụng. Để trình duyệt nhận diện và cài đặt PWA, ứng dụng cũng cần đáp ứng các điều kiện cài đặt hiện hành của trình duyệt, chẳng hạn manifest được liên kết đúng và cơ chế cài đặt của ứng dụng đã sẵn sàng.
- Dùng Chrome 152 trên hệ điều hành desktop; bài viết không giả định hỗ trợ tương đương trên mọi trình duyệt.
- Phục vụ ứng dụng qua HTTPS hoặc
localhost. Không dùng URL HTTP thông thường trên máy chủ công khai cho bước cài đặt. - Đảm bảo các tệp biểu tượng trong manifest thực sự tồn tại. Nếu dùng ví dụ dưới đây, tạo
/icons/icon-192.pngvà/icons/icon-512.png, hoặc thay bằng đường dẫn biểu tượng hợp lệ của bạn. - Có quyền sửa HTML, CSS và manifest, đồng thời có thể mở DevTools để kiểm tra tệp và lỗi cú pháp.
Nếu ứng dụng chưa thể cài thành PWA, hãy giải quyết điều kiện cài đặt trước. window-drag không phải cách thay thế cho manifest hoặc cho một ứng dụng desktop đã cài.
Bật Window Controls Overlay trong manifest
Mở tệp manifest ở thư mục gốc của ứng dụng, thường là manifest.webmanifest. Đặt nội dung tương tự sau và thay các giá trị mẫu như tên ứng dụng, start_url, scope và đường dẫn biểu tượng:
{
"name": "Desktop Notes",
"short_name": "Notes",
"start_url": "/",
"scope": "/",
"display_override": [
"window-controls-overlay",
"standalone"
],
"display": "standalone",
"background_color": "#111827",
"theme_color": "#111827",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Đặt đoạn HTML sau trong phần <head> của trang chính, rồi thay đường dẫn nếu manifest không nằm ở thư mục gốc:
<link rel="manifest" href="/manifest.webmanifest">
Chrome sẽ thử giá trị đầu tiên trong display_override; nếu không dùng được, trình duyệt có thể chuyển sang standalone. Thuộc tính display là đường lui cho môi trường không hỗ trợ cơ chế override. Sau khi sửa manifest, hãy tải lại ứng dụng và kiểm tra trong DevTools rằng tệp manifest trả về mã HTTP thành công, có nội dung JSON hợp lệ và các biểu tượng không trả về lỗi 404.
Tạo cấu trúc thanh tiêu đề và vùng tương tác
Trong HTML của trang ứng dụng, đặt thanh tiêu đề trước nội dung chính. Ví dụ dưới đây có thương hiệu, ô tìm kiếm và nút cài đặt:
<body>
<header class="app-titlebar">
<div class="titlebar-content">
<div class="brand">
<span class="brand-mark" aria-hidden="true">N</span>
<span>Desktop Notes</span>
</div>
<label class="search-box">
<span class="sr-only">Tìm ghi chú</span>
<input type="search" placeholder="Tìm ghi chú">
</label>
<button class="settings-button" type="button">
Cài đặt
</button>
</div>
</header>
<main class="page-content">
<h1>Ghi chú của tôi</h1>
<p>Nội dung ứng dụng nằm bên dưới thanh tiêu đề tùy biến.</p>
</main>
</body>
Tiếp theo, thêm CSS vào tệp stylesheet đang được trang tải. Thuộc tính window-drag: move nằm trên thanh tiêu đề; các vùng có thể nhận thao tác người dùng được loại trừ bằng window-drag: none:
:root {
--titlebar-height: 44px;
--surface: #111827;
--text: #f9fafb;
--border: #374151;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
}
body {
color: #111827;
background: #f3f4f6;
font: 15px/1.5 system-ui, sans-serif;
}
.app-titlebar {
min-height: var(--titlebar-height);
color: var(--text);
background: var(--surface);
window-drag: move;
}
.titlebar-content {
min-height: var(--titlebar-height);
display: flex;
align-items: center;
gap: 12px;
padding: 0 12px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
window-drag: none;
}
.brand-mark {
display: grid;
width: 24px;
height: 24px;
place-items: center;
border-radius: 6px;
background: #2563eb;
font-weight: 700;
}
.search-box {
flex: 1;
max-width: 420px;
window-drag: none;
}
.search-box input {
width: 100%;
min-height: 30px;
padding: 4px 10px;
color: var(--text);
background: #1f2937;
border: 1px solid var(--border);
border-radius: 6px;
}
.settings-button {
min-height: 30px;
padding: 4px 10px;
color: var(--text);
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
window-drag: none;
}
.page-content {
padding: 24px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Trong ví dụ này, vùng trống còn lại của .app-titlebar là vùng kéo. Logo, nhãn tìm kiếm, ô nhập và nút được đánh dấu none để người dùng có thể chọn, nhập hoặc nhấn chúng thay vì vô tình di chuyển cửa sổ.
Chừa vùng an toàn cho nút điều khiển cửa sổ
Window Controls Overlay đưa nội dung web vào khu vực vốn dành cho thanh tiêu đề, nhưng các nút thu nhỏ, phóng to và đóng cửa sổ vẫn do hệ điều hành hiển thị. Vị trí vùng này thay đổi theo hệ điều hành, hướng giao diện, tỷ lệ hiển thị và kích thước cửa sổ. Các biến titlebar-area-x, titlebar-area-y, titlebar-area-width và titlebar-area-height mô tả hình chữ nhật vùng title bar khả dụng; chúng không nên được hiểu máy móc là cùng một khoảng đệm trái và phải (theo developer.mozilla.org).
Có thể dùng env() để tạo khoảng đệm ban đầu. Đặt đoạn này sau quy tắc CSS trước đó:
.app-titlebar {
padding-block-start: env(titlebar-area-y, 0px);
}
.titlebar-content {
min-height: max(
var(--titlebar-height),
env(titlebar-area-height, var(--titlebar-height))
);
padding-inline-start: max(
12px,
env(titlebar-area-x, 0px)
);
padding-inline-end: max(
12px,
calc(
100% - env(titlebar-area-x, 0px) -
env(titlebar-area-width, 100%)
)
);
}
Đây là cấu hình khởi đầu, không phải khoảng cách cố định đúng cho mọi thiết kế. Nếu thanh tiêu đề có bố cục phức tạp, hãy đọc hình chữ nhật thực tế bằng API và cập nhật biến CSS của ứng dụng sau khi hình học overlay thay đổi. Hàm env() cần có giá trị dự phòng để giao diện vẫn dùng được khi trình duyệt không cung cấp biến môi trường (theo developer.mozilla.org).
Phát hiện chế độ chạy và thay đổi hình học bằng JavaScript

Không nên dùng User-Agent để kết luận ứng dụng đang chạy trong PWA. Đặt đoạn sau trong tệp JavaScript được tải sau khi tài liệu có document.documentElement. Đoạn mã chỉ cập nhật trạng thái giao diện và ghi thông tin chẩn đoán; chức năng chính của ứng dụng không được phụ thuộc vào API này:
const standaloneQuery = window.matchMedia(
'(display-mode: standalone)'
);
function updateWindowMode() {
const overlay = navigator.windowControlsOverlay;
const isStandalone = standaloneQuery.matches;
const isOverlayVisible = Boolean(overlay?.visible);
document.documentElement.dataset.pwa =
isStandalone ? 'true' : 'false';
document.documentElement.dataset.titlebarOverlay =
isOverlayVisible ? 'true' : 'false';
if (overlay) {
const rect = overlay.getTitlebarAreaRect();
console.debug('Title bar area:', {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
});
}
}
updateWindowMode();
if (navigator.windowControlsOverlay) {
navigator.windowControlsOverlay.addEventListener(
'geometrychange',
updateWindowMode
);
}
standaloneQuery.addEventListener('change', updateWindowMode);
Khi chạy trong tab, isStandalone thường là false; khi mở từ trình khởi chạy dưới dạng PWA, trạng thái này có thể chuyển thành true. Nếu navigator.windowControlsOverlay không tồn tại, hãy giữ thanh tiêu đề như HTML thông thường thay vì cố mô phỏng kéo cửa sổ bằng JavaScript.
Cài đặt và xác minh theo đúng thứ tự
- Phục vụ ứng dụng: khởi động máy chủ phát triển hoặc máy chủ HTTPS của dự án. Mở đúng URL ứng dụng trong Chrome 152 trên máy tính.
- Kiểm tra manifest: trong DevTools, mở phần Application rồi kiểm tra manifest,
display_override,display, biểu tượng và các đường dẫn. Sửa mọi lỗi JSON hoặc lỗi tải tệp trước khi tiếp tục. - Cài PWA: dùng nút cài đặt PWA của Chrome, không chỉ ghim tab. Nếu ứng dụng đã cài từ trước, hãy đóng cửa sổ ứng dụng, tải lại manifest và cài lại khi cần để kiểm tra thay đổi.
- Mở đúng ứng dụng: đóng tab gốc, sau đó mở PWA từ shortcut hoặc trình khởi chạy của hệ điều hành.
- Kiểm tra vùng kéo: nhấn và kéo phần trống của
.app-titlebar. Kết quả mong đợi là cửa sổ PWA di chuyển theo chuột. - Kiểm tra vùng loại trừ: nhấn nút Cài đặt, đặt con trỏ vào ô tìm kiếm, nhập văn bản và thử chọn văn bản. Các thao tác này phải diễn ra trong trang, không kéo cửa sổ.
- Kiểm tra vùng hệ thống: thay đổi kích thước cửa sổ, thử trạng thái phóng to và kiểm tra khu vực gần các nút hệ thống. Logo, tiêu đề và nút ứng dụng không được bị che.
- Kiểm tra fallback: mở cùng URL trong tab thông thường hoặc môi trường không có Window Controls Overlay. Nội dung vẫn phải đọc được, điều khiển vẫn phải dùng được và thanh đầu trang không được tạo cảm giác là vùng kéo cửa sổ.
Nếu bước kiểm tra đầu tiên không kéo được cửa sổ, đừng chỉnh màu sắc hoặc kích thước trước. Hãy xác định ứng dụng có thực sự được cài, manifest có được đọc lại và API có hiện diện hay không.
Xử lý lỗi theo triệu chứng
Thanh tiêu đề không kéo được
- Kiểm tra bạn đang mở cửa sổ PWA đã cài, không phải URL trong tab.
- Kiểm tra manifest có chính xác
window-controls-overlay, cóstandalonedự phòng và được liên kết từ trang hiện tại. - Kiểm tra DevTools không báo lỗi cú pháp CSS hoặc lỗi tải stylesheet.
- Kiểm tra vùng cần kéo không bị một phần tử phủ lên và không bị áp dụng
window-drag: nonengoài ý muốn. - Kiểm tra trình duyệt có cung cấp
navigator.windowControlsOverlayvà trạng thái hiển thị phù hợp. Nếu không, hành vi không kéo được có thể là fallback đúng, không phải lỗi CSS.
Nút hoặc ô nhập liệu bị kéo thay vì nhận thao tác
Đặt window-drag: none trực tiếp lên các phần tử nhận tương tác. Với thanh tiêu đề có nhiều loại điều khiển, có thể thêm quy tắc sau:
.titlebar-content button,
.titlebar-content input,
.titlebar-content select,
.titlebar-content textarea,
.titlebar-content a,
.titlebar-content label {
window-drag: none;
}
Đừng chỉ loại trừ nút mà bỏ qua label bao quanh ô nhập hoặc phần tử phủ lên điều khiển. Sau khi sửa CSS, tải lại cửa sổ PWA rồi kiểm tra cả thao tác chuột và bàn phím.
Nội dung bị che bởi nút đóng hoặc phóng to
Không đặt logo hoặc nút ứng dụng cố định ở góc trên cùng mà không tính đến hình chữ nhật title bar. Kiểm tra giá trị rect được ghi trong Console, đối chiếu với vị trí thực tế của nội dung và điều chỉnh khoảng đệm. Nếu bố cục dùng RTL hoặc nhiều màn hình, kiểm tra thêm hướng giao diện và tỷ lệ hiển thị thực tế.
Chạy được trong Chrome 152 nhưng không dùng được ở trình duyệt khác
Không giả định mọi trình duyệt đều hỗ trợ cùng một thuộc tính hoặc API. Fallback sau đây tắt vùng kéo khi thuộc tính không được nhận diện và tắt khoảng đệm dành riêng cho overlay khi trạng thái overlay là false:
@supports not (window-drag: move) {
.app-titlebar {
window-drag: none;
}
}
html[data-titlebar-overlay="false"] .app-titlebar {
padding-block-start: 0;
}
Không dùng Pointer Events để mô phỏng việc di chuyển cửa sổ. Cách đó chỉ di chuyển nội dung trong viewport, không di chuyển cửa sổ cấp hệ điều hành.
Khả năng sử dụng, bảo mật và đường lui
- Giữ tên ứng dụng, nhận diện nguồn gốc và điều hướng rõ ràng; giao diện gần như không có thanh tiêu đề hệ thống có thể khiến người dùng khó phân biệt vùng web với vùng cửa sổ.
- Không biến toàn bộ màn hình thành vùng kéo. Người dùng vẫn phải chọn văn bản, cuộn nội dung và thao tác bằng bàn phím.
- Đảm bảo mọi nút và ô nhập liệu có nhãn, thứ tự bàn phím và trạng thái tiêu điểm phù hợp.
- Kiểm tra trạng thái phóng to, thu nhỏ, nhiều màn hình, tỷ lệ hiển thị lớn và RTL nếu ứng dụng hỗ trợ các trường hợp đó.
- Chỉ giữ Window Controls Overlay khi lợi ích của thanh tiêu đề tùy biến đáng để kiểm thử trên các hệ điều hành và trình duyệt mục tiêu.
Nếu tính năng gây lỗi hoặc khó bảo trì, đường lui an toàn là xóa window-controls-overlay khỏi display_override, giữ display: "standalone" và đặt window-drag: none cho thanh tiêu đề. Ứng dụng vẫn có thể chạy như PWA standalone với cấu trúc HTML và các điều khiển thông thường.
Bạn có thể coi triển khai hoàn tất khi cả bốn điều kiện đều đúng: PWA đã cài trên desktop; vùng trống của thanh tiêu đề di chuyển được cửa sổ; nút và ô nhập liệu vẫn hoạt động; nội dung không bị che bởi vùng điều khiển hệ thống. Nếu thiếu một điều kiện, quay lại kiểm tra manifest, trạng thái cài đặt, vùng phủ phần tử và các quy tắc window-drag: none.

