🌐 한국어
Class-based, framework-neutral CSS components. Everything references semantic
tokens only (var(--dds-*)) — load
@devslab/dds-tokens/tokens.css first, then @devslab/dds-css/dds.css
(or per-component files from components/). Dark mode needs no component
change: set data-theme="dark" on <html> or any subtree
(data-theme="light" pins a subtree back).
Every interactive component defines the full state set of spec §4.2 (default → hover → pressed → focus-visible → disabled → loading) and passes the §6 accessibility floor: 4.5:1 text contrast, ≥24×24 web click targets, keyboard reachability with a visible 2px focus ring, no color-only signaling.
Live reference: preview/components.html (run pnpm build first).
Shipped (spec §4.3 v1 inventory): Button, IconButton, TextField, Textarea, Select, Checkbox/Radio, Switch, Badge, Chip, Avatar, Spinner, Skeleton, Divider, Card, ListRow, Tabs, Dialog (web Modal), Toast, Tooltip (web), EmptyState. BottomSheet is deliberately absent: it is the native counterpart of Dialog, and this package is web CSS — the role mapping (Modal on web ↔ BottomSheet on native) is documented in spec §4.3/§5 and the native implementation waits for a native consumer (Phase 3).
<button class="dds-btn dds-btn--primary">Save</button>
<button class="dds-btn dds-btn--secondary dds-btn--sm">Cancel</button>
<button class="dds-btn dds-btn--primary" aria-busy="true" disabled>
<span class="dds-spinner" aria-hidden="true"></span>Save
</button>- Variants:
--primary--secondary--ghost--danger. Sizes:--sm(32px) / default (40px) /--lg(48px) — heights from the spacing scale. - States: hover/pressed via tokens (
bg.brand-hover, alpha tints); disabled =[disabled](opacity 0.45 — the one allowed opacity state); loading =aria-busy="true"+ a.dds-spinnerchild (blocks pointer events). Loading keeps the button's size (D-043): the label and icons stay in place and in the accessibility tree, so the accessible name does not change, but they are not painted; the spinner is centred over them in the button's text color. Neighbours do not move and a wrapped label does not wrap again. A loading button is not dimmed, even with[disabled](the framework Buttons set both), so the spinner keeps 3:1 or better. In forced colors it uses system colors (button face,GrayTextborder,ButtonTextspinner). - Keep the label while loading. The hidden label still sets the width, so swapping "Save" for "Saving…" resizes the button. To tell screen reader users that the work finished or failed, use a status message (Toast).
- A11y: primary text is
on-brand(zinc.950, ~8:1 — spec §3.1 note); danger text ison-status(white/light, dark/zinc.950); focus ring 2pxborder.focusoffset 2px; icon-only buttons must addaria-label. - Links as buttons:
<a class="dds-btn dds-btn--primary" href="…">gets the same look — the base rule resets the anchor underline, so no consumer shim is needed. - Do: one primary per view; pair danger with a confirm step.
- Don't: don't fake a disabled state with a ghost variant — use
[disabled]so it's out of the tab order and announced correctly.
<button class="dds-iconbtn" aria-label="Close">
<svg aria-hidden="true" width="20" height="20">…</svg>
</button>
<button class="dds-iconbtn dds-iconbtn--secondary dds-iconbtn--sm" aria-label="Edit">…</button>
<button class="dds-iconbtn dds-iconbtn--danger" aria-label="Delete source">…</button>- Variants: default (ghost) /
--secondary(bordered) /--danger. Sizes:--sm(32) / default (40) /--lg(48) — the same heights as.dds-btn, so an icon button never sits a few px off the text button beside it. - The icon inherits
currentColor; give the<svg>aria-hidden="true". - Loading (
aria-busy="true"; the frameworkloadingprop swaps the icon for a.dds-spinner): same box, samearia-label, pointer blocked and, like Button, not dimmed (D-043). - A11y:
aria-labelis mandatory — an icon has no accessible name.--sm(32×32) clears the 24×24 web floor but not the 44×44 touch target: on touch surfaces use the default size. - Do: pair with a Tooltip for discoverability, but keep the
aria-labelas the real name (the tooltip isaria-describedby, not the label). - Don't: don't mix sizes inside one action row — pick one and keep every row action on it.
<div class="dds-field">
<label class="dds-field__label" for="email">Email</label>
<input class="dds-input" id="email" type="email"
aria-invalid="true" aria-describedby="email-help">
<span class="dds-field__help" id="email-help">Not an email address.</span>
</div>- Error:
.dds-field--erroron the wrapper (oraria-invalid="true"on the input — both style the border; use aria-invalid so AT hears it too). - A11y: always a real
<label for>; error/help text linked viaaria-describedby; the red border is never the only error signal — the help line carries the message (§6 no-color-only). - Do: keep help text present in both normal and error states so the layout doesn't jump.
- Don't: don't use
placeholderas the label — it disappears on input and fails contrast as a label substitute.
<div class="dds-field">
<label class="dds-field__label" for="intro">Introduction</label>
<textarea class="dds-textarea" id="intro" rows="4"
aria-describedby="intro-help"></textarea>
<span class="dds-field__help" id="intro-help">Shown to visitors.</span>
</div>- Lives in the same
.dds-fieldwrapper as.dds-input: label, help and error (.dds-field--error/aria-invalid) behave identically. min-heightis two control heights,resize: vertical(horizontal resize breaks the grid the form sits in).- Do: give a textarea its own form row. Mixing a one-line control and a textarea in one row is a rejection criterion in this house — their heights cannot be reconciled without shrinking one of them.
- Don't: don't grow it by shrinking the control next to it; don't disable resize entirely when the content is genuinely long-form.
<div class="dds-field">
<label class="dds-field__label" for="locale">Language</label>
<span class="dds-select">
<select class="dds-select__input" id="locale">
<option>한국어</option><option>English</option>
</select>
</span>
</div>- A real
<select>(keyboard, mobile picker, form semantics) with the native arrow replaced by the wrapper's chevron. Leaving the OS arrow in place is a rejection criterion — the same control then looks different on every platform and mismatches the input next to it. - The wrapper
.dds-selectis required (it draws the chevron); the class on the element itself is.dds-select__input. - A11y: label via
<label for>like any field; the chevron ispointer-events: noneso clicks always reach the select. - Do: build option lists from the data (distinct values), not a hardcoded list.
- Don't: don't replace a
<select>with a div+listbox unless you are also implementing the full ARIA listbox pattern.
<label class="dds-check">
<input class="dds-check__input" type="checkbox" checked>
Send me the weekly digest
</label>
<label class="dds-check">
<input class="dds-check__input" type="radio" name="cadence" value="daily">
Daily
</label>- One class for both: the input's
typepicks the shape (square + tick vs circle + dot).:indeterminateis styled for "some selected" headers. - The tick/dot is
on-brand(zinc.950), never white — same rule as the primary button (spec §3.1). - A11y: wrap the text in the
<label>so the whole row is the hit area (the 18px box alone is below the touch floor); disabled dims the label once, not the box twice. - Do: radio for one-of-many that takes effect on submit; checkbox for independent options.
- Don't: don't use a checkbox for a setting that applies immediately — that is a Switch.
<label class="dds-switch">
<input class="dds-switch__input" type="checkbox" role="switch" checked>
Handoff alerts
</label>- An immediate on/off. Track 36×20, knob 16 (glyph geometry, not spacing).
The knob turns
on-brandon the brand-filled track — the same rule that keeps white off cyan. - Transitions are removed under
prefers-reduced-motion. - A11y:
role="switch"on the input so AT announces on/off rather than checked/unchecked; the label text is the accessible name. - Do: apply the change immediately and report failure with a toast — silent no-op is a rejection criterion.
- Don't: don't put a Save button next to a switch; if the value needs saving, it's a checkbox.
<span class="dds-badge dds-badge--success">Connected</span>- Variants:
--brand--success--warning--danger--info. - Colors are always the paired set (dark foreground +
-bgtint, spec §3.1); the leading dot repeats the tone so color isn't the only signal. - Do: use for state, not for actions.
- Don't: don't invent new fg/bg combinations — the pairs are the tokens'
contract (
status.*+status.*-bg).
<button class="dds-chip" aria-pressed="true">Unanswered</button>
<button class="dds-chip">Web pages</button>
<button class="dds-chip" disabled>Archived</button>- A small interactive control: filter toggle, suggested question,
removable tag. Badge and Chip are different components on purpose — a badge
is state you read, a chip is something you press. Never restyle one into
the other (a duplicated
.chipdefinition has already cost this house a live bug). - Selected =
aria-pressed="true"(announced);--selectedexists only for markup that can't usearia-pressed(e.g. a link chip). - Do: keep chips one line high and let the row scroll; label them with the value, not "Filter 1".
- Don't: don't use a chip as the primary action of a view — that's a Button.
<span class="dds-avatar" aria-hidden="true">KS</span>
<span class="dds-avatar dds-avatar--lg">
<img class="dds-avatar__img" src="/u/12.jpg" alt="">
</span>
<span class="dds-avatar dds-avatar--square dds-avatar--sm" aria-hidden="true">DL</span>- Sizes 32 / 40 / 48 from the spacing scale, so an avatar lines up with the
control of the same size in a list row.
--squarefor company/tenant marks (a logo in a circle gets clipped). - Initials use the brand-subtle pair, so they re-tint with the product brand.
- A11y: the avatar is decorative when the name is already in the row —
aria-hidden="true"on initials,alt=""on the image. Only give it an alt/label when it is the only identification. - Do: derive initials from the same name string the row shows.
- Don't: don't put status text inside the avatar — use a Badge next to it.
<span class="dds-spinner" role="status" aria-label="Loading"></span>
<div aria-busy="true">
<span class="dds-skeleton dds-skeleton--circle" aria-hidden="true" style="width:40px;height:40px"></span>
<span class="dds-skeleton dds-skeleton--text" aria-hidden="true" style="width:60%"></span>
</div>- Spinner inherits
currentColor(correct on any surface, including inside a primary button). Standalone spinners needrole="status"+aria-label. Under reduced motion it slows instead of freezing — a stopped spinner reads as a hang. In forced colors it paints itself inCanvasTextso its gap stays open (forced colors would otherwise fill the transparent top and leave a ring that looks still). - Skeleton fill is an alpha tint (works on default and subtle backgrounds in
both themes). Size is the consumer's (width/height inline or via layout);
--textis one line,--circlefor avatars. Mark the regionaria-busy="true", keep skeleton elementsaria-hidden="true"; the pulse stops under reduced motion. - Do: skeleton for content-shaped waits, spinner for action-shaped waits.
- Don't: don't announce each skeleton to AT — one busy region is enough.
<hr class="dds-divider">
<div style="display:flex">A<hr class="dds-divider dds-divider--vertical">B</div>--verticalneeds a stretchable parent (flex row / grid cell).- Do: use
<hr>when the split is meaningful; addrole="presentation"when it is purely decorative. - Don't: don't use a divider where spacing already separates the groups — two separators in a row is visual noise.
<section class="dds-card">
<h3 class="dds-card__title">Reindex schedule</h3>
<p class="dds-card__body">Sources are recrawled daily at 04:00 KST.</p>
</section>
<section class="dds-card dds-card--subtle">…</section>- Border +
bg.elevated+elevation.1. The shadow token ships in both themes: on a zinc.950 page it resolves to almost nothing, which is the intended dark look — a component never branches on theme (spec §3.1). --subtleis the flat variant for a card inside an already elevated surface (a card in a dialog), where a second shadow reads as noise.- Do: one card = one subject; put its actions in a
.dds-cardfooter row of buttons. - Don't: don't nest full cards inside cards — use
--subtleor a Divider.
<ul style="list-style:none;margin:0;padding:0">
<li><button class="dds-listrow dds-listrow--interactive">
<span class="dds-avatar dds-avatar--sm" aria-hidden="true">KS</span>
<span class="dds-listrow__body">
<span class="dds-listrow__title">강신</span>
<span class="dds-listrow__sub">대표 · 초대함</span>
</span>
<span class="dds-badge dds-badge--success">활성</span>
</button></li>
</ul>- Slots: leading (any flex child),
__body(__title+__sub),__actions. Rows separate themselves (+ .dds-listrowdraws the rule). --interactivefor rows that are the control — use a real<button>or<a>. The focus ring is inset (a full-bleed row has no outer margin).- Long titles ellipsize; the row height stays constant.
- Do: make the row self-contained — if the information fits in the row, don't defer it to a detail page.
- Don't: don't put an inline accordion in a row that can be deleted or edited — a re-render after the action collapses it. Use a dedicated view.
<div class="dds-tabs" role="tablist">
<button class="dds-tab" role="tab" aria-selected="true" aria-controls="p1" id="t1">Sources</button>
<button class="dds-tab" role="tab" aria-selected="false" aria-controls="p2" id="t2">Visitors</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">…</div>- Ink-underline tabs, horizontally scrollable when they overflow (the mobile behavior of the admin shell).
- Selection is
aria-selected, never a class alone — the state must be announced, and the underline repeats it so color isn't the only signal. - A11y:
role="tablist"/tab/tabpanelwitharia-controls+aria-labelledby; arrow-key roving focus is the consumer's JS. - Do: group tabs by kind and put a new feature in the obvious existing group — a flat list of everything is just a stack with a new name.
- Don't: don't hide a tab's panel with a
displayrule that lacks a[hidden]guard (that exact collision once painted an empty state over a populated table).
<div class="dds-dialog-overlay">
<div class="dds-dialog" role="dialog" aria-modal="true" aria-labelledby="t">
<h2 class="dds-dialog__title" id="t">Reindex sources?</h2>
<p class="dds-dialog__body">Changes apply to answers immediately.</p>
<div class="dds-dialog__actions">
<button class="dds-btn dds-btn--ghost dds-btn--sm">Later</button>
<button class="dds-btn dds-btn--primary dds-btn--sm">Reindex</button>
</div>
</div>
</div>- Native
<dialog class="dds-dialog">is also styled (incl.::backdrop). - Behavior contract (spec §5): closes on outside tap and Esc; focus moves in
on open, is trapped while open, returns to the opener on close — that's
the opening code's job (native
<dialog>.showModal()gives you most of it). - A11y:
aria-modal="true"+aria-labelledbyto the title. - Do: actions right-aligned, primary last (matches the modal convention used across DevsLab admin surfaces).
- Don't: don't stack dialogs; don't use a dialog for non-blocking notices — that's the toast's job.
React/Solid/Vue loading, update, promise and action APIs are documented in the
Toast guide. The CSS-only markup below remains supported. Loading
uses .dds-spinner[aria-hidden="true"]; actionable framework toasts use
.dds-toast--action with a .dds-toast__action[data-part="action-trigger"]
button beneath the message.
<div class="dds-toast-region" role="status">
<div class="dds-toast dds-toast--success">Indexing finished.</div>
<div class="dds-toast dds-toast--danger" role="alert">Sending failed.</div>
</div>- Region is fixed bottom-end; toasts stack with a grid gap. Variants add the
status dot (same 6px dot as the badge). Entry animation uses
duration.base+easing.enterand is removed under reduced motion. - A11y: region
role="status"(polite); userole="alert"only on danger toasts. Auto-dismiss should pause on hover/focus (consumer JS). - Do: every failed action gets a toast — silent failure is a rejection criterion in this house.
- Don't: don't put required actions only inside a toast (it disappears); don't stack more than ~3 — collapse older ones.
<span class="dds-tooltip">
<button class="dds-iconbtn" aria-label="Recrawl" aria-describedby="tp1">
<svg aria-hidden="true" width="20" height="20">…</svg>
</button>
<span class="dds-tooltip__bubble" role="tooltip" id="tp1">Recrawl now</span>
</span>- Web only (spec §4.3). On native the same information goes in a label or a sheet — there is no hover.
- Opens on hover and
:focus-within, so it is keyboard-reachable; the bubble sits onbg.inverse/text.on-inverse(D-011). - A11y: reference it with
aria-describedby, notaria-label— the control keeps its own name, and a describedby bubble reaches AT even when it never visually opens. - Do: use it for the name of an icon-only control or a short unit hint.
- Don't: never put information required to use the control in a tooltip — it is unavailable on touch and disappears on scroll.
<div class="dds-empty">
<p class="dds-empty__title">등록된 자료가 없습니다</p>
<p class="dds-empty__desc">웹페이지 주소나 PDF를 추가하면 AI가 그 내용으로 답합니다.</p>
<div class="dds-empty__actions">
<button class="dds-btn dds-btn--primary dds-btn--sm">자료 추가</button>
</div>
</div>- The empty view keeps the same skeleton as the filled one — summary chips and filters stay put; the empty state replaces the rows, not the screen.
- Always name the next action in
__actions; an empty state without a way forward is a dead end. - A11y: it is ordinary content, not an alert. When it appears after a search, put the result count in the live region instead.
- Do: distinguish "nothing yet" (onboarding copy) from "nothing matched" (offer to clear the filter).
- Don't: don't toggle it with a
displayrule that outranks[hidden]—.dds-empty[hidden]is guarded here for exactly that reason.
The two sections below shipped after the spec §4.3 v1 inventory above and
are not part of it — Table and Console shell arrived with the console
migration into DDS (docs/decisions.md D-021). They follow the same
reference conventions as every section above (real shipped classes only,
usage snippet, A11y notes, Do/Don't).
<div class="dds-table-wrap">
<table class="dds-table dds-table--fixed">
<caption class="dds-visually-hidden">Jobs</caption>
<colgroup>
<col style="width: 60%" />
<col data-fold style="width: 40%" />
</colgroup>
<thead>
<tr>
<th scope="col" data-column="name" aria-sort="ascending">
<button type="button" class="dds-table__sort" aria-label="Sort by Name">
Name
<span class="dds-table__sort-mark" aria-hidden="true" data-sort-state="ascending"></span>
</button>
</th>
<th scope="col" data-column="count" data-fold data-numeric>Count</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Reindex</th>
<td data-fold data-numeric>12</td>
</tr>
</tbody>
</table>
</div>
<nav class="dds-table__pagination" aria-label="Next page">
<a class="dds-btn dds-btn--secondary" href="?cursor=abc">Next page</a>
</nav>- Wrap:
.dds-table-wrapscrolls horizontally on overflow and is the positioned ancestor an absolutely-clipped.dds-visually-hiddencaption resolves against;--tallcaps it at 70vh and pins the header (thead th) withposition: sticky, and switches its table toborder-collapse: separate(zero spacing) — collapsed borders can keep a sticky header from sticking. - Density and layout:
.dds-table--denseshrinks row height from the--dds-space-40token (40px) to 2.25rem (36px);.dds-table--fixedswitches totable-layout: fixedand expects a<colgroup>— the component applies both automatically the moment any column declares awidth. - Sort:
.dds-table__sortis a sortable header's button (rendered only for a client-sortable column);.dds-table__sort-markis its chevron, itsdata-sort-stateattribute holding"none"/"ascending"/"descending"— the same three values the header's ownaria-sortcarries. .dds-table__orderrenders the stated-order sentence above the table when the caller supplies one instead of a sort button (see thesortprop below)..dds-table__actionsright-aligns the trailing actions cell's content; the actions column's own header uses the.dds-visually-hiddenutility so it still has an accessible name without a visible one.- Row actions: a
.dds-btninside.dds-table__actionsis 32px whatever its size modifier and never wraps, and the actions cell trims its block padding to 4px — a row with a button is as tall as the same row without one. Any cell holding a.dds-btn--sm(a copy button beside an id) trims its padding the same way. In.dds-table--dense, row actions and every.dds-btn--smin a cell are 24px. On a coarse pointer all of them return to the 44px touch floor and the row grows instead (D-025). - A
<code>in a cell takes the mono family at 13px, for ids, keys and hashes read character by character. .dds-table__paginationis a labeled<nav>wrapping a plain.dds-btn.dds-btn--secondarylink — never a button — so a cursor-paged next page keeps working with JavaScript off; it is[hidden]entirely when there is no next page.- Attribute hooks:
[data-fold]marks a column (<col>,<th>,<td>) droppable — hidden outright below56.25rem(900px) by a plain media query, no JavaScript;[data-numeric]right-aligns a cell and gives it tabular figures;[data-sort-state](above) carries the three sort states;[data-column]names the column for a consumer's own hooks and carries no CSS of its own. - A11y: the caption is real, only visually hidden; a sortable header is
a real
<button>witharia-label, never a bare clickable<th>; exactly one column per table should render as<th scope="row">rather than<td>. - Do: give every table a caption, even a visually hidden one — it is the table's accessible name.
- Don't: don't offer a sort button on a table whose data is server-
ordered — a client sort of one paged slice would misreport the whole
list's order, so this markup never renders one in that case (
sort's{ statedOrder }form; see@devslab/dds-table's README). - Don't: don't re-size row actions or cell buttons in product CSS — the table already holds them to its row height, and a product override out-specifying these rules will also defeat their touch-floor restatement.
<div class="dds-console-shell" data-surface="dashboard" data-hydrated="true">
<a class="dds-console-skip dds-visually-hidden" href="#dds-console-main">Skip to content</a>
<button class="dds-console-rail__toggle" type="button" aria-controls="dds-console-rail" aria-expanded="false">
<span aria-hidden="true"></span>
</button>
<div class="dds-console-rail__scrim" data-open="false"></div>
<aside id="dds-console-rail" class="dds-console-rail" data-open="false">
<a class="dds-console-rail__brand" href="/">
<img src="/mark.svg" alt="" width="20" height="20" /><strong>Acme</strong>
</a>
<nav class="dds-console-rail__nav" aria-label="Dashboard navigation">
<section class="dds-console-rail__group" aria-labelledby="g1">
<h2 id="g1" class="dds-console-rail__group-label">Today</h2>
<ul>
<li><a class="dds-console-rail__item" href="/overview" aria-current="page">
Overview
<span class="dds-console-rail__badge" aria-label="3 pending">3</span>
</a></li>
</ul>
</section>
</nav>
</aside>
<main id="dds-console-main" class="dds-console-main" tabindex="-1">
<header class="dds-console-page-header">
<div class="dds-console-page-header__text">
<p class="dds-console-page-eyebrow">Acme</p>
<h1>Overview</h1>
</div>
</header>
…
</main>
</div>- Layout:
.dds-console-shellis the 14.5rem (232px)-rail-plus-content CSS grid; below56.25rem(900px) — the same breakpoint the table's[data-fold]uses, so a rail and a table folding column change shape at the same viewport width — the grid collapses to one column and.dds-console-railbecomes a fixed, off-canvas drawer. - Rail parts:
.dds-console-rail__brand,__context(a<dl>of label/value rows),__nav, one__groupper labeled section (+__group-label),__item(with hover /:focus-visible/aria-current="page"states),__badge(a pending-count pill), and__foot(a bottom slot, pinned withmargin-block-start: auto). - Drawer (active only below 900px):
.dds-console-rail__toggleisdisplay: noneabove the breakpoint and the fixed hamburger button below it — it exposes its state asaria-expanded, notdata-open, since it's a disclosure control rather than the thing being disclosed; its own accessible name (see the visually-hidden section below) switches with that same state..dds-console-rail__scrimis likewise hidden above the breakpoint and a full-screen dimmer below it. The scrim and the rail itself (not the toggle) readdata-open="true"ordata-open="false"— always one or the other, never absent — which is what lets the drawer's open/close CSS (translate,visibility, the scrim's opacity/pointer-events) target[data-open="true"]directly rather than relying on the attribute's presence, the way[data-fold]or[hidden]do elsewhere in this catalog.prefers-reduced-motion: reduceremoves the drawer's transition. data-surfacenames the console ("dashboard","admin", …) for a consumer's own hooks and carries no CSS.data-hydrated="true"appears only once client-side hydration has actually run — it is absent, not"false", on first paint and in any environment hydration never reaches..dds-console-mainis the skip link's target (id="dds-console-main",tabindex="-1") and holds.dds-console-page-header—__text(.dds-console-page-eyebrow+<h1>) and, when the caller supplies one,__actionsto the header's right.- Density: any
.dds-btn,.dds-input,.dds-select, or.dds-textarearendered inside.dds-console-shellrenders at the body-2 (14px) size instead of its own default — a console establishes this once for everything inside it rather than each control overriding itself. - A11y: the skip link is real markup, hidden with
.dds-visually-hiddenuntil it receives focus; the rail's<nav>requires a caller-suppliedaria-label— a console page also has the table's own pagination<nav>(above), so an unnamed landmark next to a named one would be a regression; every class in this stylesheet that setsdisplayre-declares[hidden] { display: none }beside it, including inside the drawer media query, so a hidden rail/toggle/scrim can never be forced back on screen regardless of viewport width. - Do: use the badge for a real pending count, not a decorative dot — its accessible name is filled from a caller-supplied label template, not a word this package chose.
- Don't: don't reach for
[hidden]to open or close the drawer — the component already ownsdata-open, and the CSS above depends on that exact string value, not on the attribute's presence or absence.
Visually hidden — .dds-visually-hidden
<span class="dds-visually-hidden">Actions</span>- A utility in
base.css, not tied to any one component — it clips content to a 1px box (clip-path: inset(50%)) so it takes no visual space but stays in the accessibility tree, unlike[hidden](ordisplay: none), which removes an element from the tree too. - Its containing block should be
position: relativefor the clip to resolve against that box rather than the page —.dds-table-wrapsets this for exactly that reason, so the table's own caption clips correctly even while the wrap is scrolled horizontally. - Shipped consumers: the table's caption and its actions-column header
above; the console shell's skip link (visible only once it receives
focus, via its own
:focusrule); and the console shell's rail-toggle button, whose accessible name (themenuOpen/menuCloselabel) is a visually-hidden span that swaps text as the drawer opens and closes. - Do: use it for text a screen reader needs but a sighted user does not — a caption, a landmark's accessible name, an icon-only control's label.
- Don't: don't reach for it to hide something temporarily from
everyone — that removes nothing from the accessibility tree, which is
exactly wrong for content nobody should perceive right now; use
[hidden]for that instead.