Skip to content

Latest commit

 

History

History
669 lines (592 loc) · 30.8 KB

components.md

File metadata and controls

669 lines (592 loc) · 30.8 KB

DDS Components — v1 inventory (@devslab/dds-css)

🌐 한국어

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 — .dds-btn

<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-spinner child (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, GrayText border, ButtonText spinner).
  • 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 is on-status (white/light, dark/zinc.950); focus ring 2px border.focus offset 2px; icon-only buttons must add aria-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.

IconButton — .dds-iconbtn

<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 framework loading prop swaps the icon for a .dds-spinner): same box, same aria-label, pointer blocked and, like Button, not dimmed (D-043).
  • A11y: aria-label is 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-label as the real name (the tooltip is aria-describedby, not the label).
  • Don't: don't mix sizes inside one action row — pick one and keep every row action on it.

TextField — .dds-field / .dds-input

<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--error on the wrapper (or aria-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 via aria-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 placeholder as the label — it disappears on input and fails contrast as a label substitute.

Textarea — .dds-textarea

<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-field wrapper as .dds-input: label, help and error (.dds-field--error / aria-invalid) behave identically.
  • min-height is 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.

Select — .dds-select / .dds-select__input

<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-select is 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 is pointer-events: none so 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.

Checkbox / Radio — .dds-check / .dds-check__input

<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 type picks the shape (square + tick vs circle + dot). :indeterminate is 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.

Switch — .dds-switch / .dds-switch__input

<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-brand on 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.

Badge — .dds-badge

<span class="dds-badge dds-badge--success">Connected</span>
  • Variants: --brand --success --warning --danger --info.
  • Colors are always the paired set (dark foreground + -bg tint, 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).

Chip — .dds-chip

<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 .chip definition has already cost this house a live bug).
  • Selected = aria-pressed="true" (announced); --selected exists only for markup that can't use aria-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.

Avatar — .dds-avatar

<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. --square for 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.

Spinner .dds-spinner / Skeleton .dds-skeleton

<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 need role="status" + aria-label. Under reduced motion it slows instead of freezing — a stopped spinner reads as a hang. In forced colors it paints itself in CanvasText so 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); --text is one line, --circle for avatars. Mark the region aria-busy="true", keep skeleton elements aria-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.

Divider — .dds-divider

<hr class="dds-divider">
<div style="display:flex">A<hr class="dds-divider dds-divider--vertical">B</div>
  • --vertical needs a stretchable parent (flex row / grid cell).
  • Do: use <hr> when the split is meaningful; add role="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.

Card — .dds-card

<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).
  • --subtle is 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-card footer row of buttons.
  • Don't: don't nest full cards inside cards — use --subtle or a Divider.

ListRow — .dds-listrow

<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-listrow draws the rule).
  • --interactive for 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.

Tabs — .dds-tabs / .dds-tab

<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/tabpanel with aria-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 display rule that lacks a [hidden] guard (that exact collision once painted an empty state over a populated table).

Dialog — .dds-dialog

<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-labelledby to 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.

Toast — .dds-toast

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.enter and is removed under reduced motion.
  • A11y: region role="status" (polite); use role="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.

Tooltip — .dds-tooltip / .dds-tooltip__bubble

<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 on bg.inverse / text.on-inverse (D-011).
  • A11y: reference it with aria-describedby, not aria-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.

EmptyState — .dds-empty

<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 display rule that outranks [hidden] — .dds-empty[hidden] is guarded here for exactly that reason.

Post-v1 components

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).

Table — .dds-table

<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-wrap scrolls horizontally on overflow and is the positioned ancestor an absolutely-clipped .dds-visually-hidden caption resolves against; --tall caps it at 70vh and pins the header (thead th) with position: sticky, and switches its table to border-collapse: separate (zero spacing) — collapsed borders can keep a sticky header from sticking.
  • Density and layout: .dds-table--dense shrinks row height from the --dds-space-40 token (40px) to 2.25rem (36px); .dds-table--fixed switches to table-layout: fixed and expects a <colgroup> — the component applies both automatically the moment any column declares a width.
  • Sort: .dds-table__sort is a sortable header's button (rendered only for a client-sortable column); .dds-table__sort-mark is its chevron, its data-sort-state attribute holding "none" / "ascending" / "descending" — the same three values the header's own aria-sort carries.
  • .dds-table__order renders the stated-order sentence above the table when the caller supplies one instead of a sort button (see the sort prop below).
  • .dds-table__actions right-aligns the trailing actions cell's content; the actions column's own header uses the .dds-visually-hidden utility so it still has an accessible name without a visible one.
  • Row actions: a .dds-btn inside .dds-table__actions is 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--sm in 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__pagination is a labeled <nav> wrapping a plain .dds-btn.dds-btn--secondary link — 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 below 56.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> with aria-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.

Console shell — .dds-console-shell

<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-shell is the 14.5rem (232px)-rail-plus-content CSS grid; below 56.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-rail becomes a fixed, off-canvas drawer.
  • Rail parts: .dds-console-rail__brand, __context (a <dl> of label/value rows), __nav, one __group per labeled section (+ __group-label), __item (with hover / :focus-visible / aria-current="page" states), __badge (a pending-count pill), and __foot (a bottom slot, pinned with margin-block-start: auto).
  • Drawer (active only below 900px): .dds-console-rail__toggle is display: none above the breakpoint and the fixed hamburger button below it — it exposes its state as aria-expanded, not data-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__scrim is likewise hidden above the breakpoint and a full-screen dimmer below it. The scrim and the rail itself (not the toggle) read data-open="true" or data-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: reduce removes the drawer's transition.
  • data-surface names 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-main is 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, __actions to the header's right.
  • Density: any .dds-btn, .dds-input, .dds-select, or .dds-textarea rendered inside .dds-console-shell renders 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-hidden until it receives focus; the rail's <nav> requires a caller-supplied aria-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 sets display re-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 owns data-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] (or display: none), which removes an element from the tree too.
  • Its containing block should be position: relative for the clip to resolve against that box rather than the page — .dds-table-wrap sets 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 :focus rule); and the console shell's rail-toggle button, whose accessible name (the menuOpen/menuClose label) 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.