The project is organized into separate folders to keep related files together and make future development easier.
attribute-app/
│
├── index.html
├── add-attribute.html
├── edit-attribute.html
│
├── css/
├── js/
├── assets/
├── data/
│
├── README.md
└── BUGS.md
The <meta charset> tag should be placed near the beginning of the <head> section because browsers need to know the character encoding before reading the HTML. It should appear within the first 1024 bytes of the document so the browser can correctly decode the page.
The theme-color meta tag controls the browser UI color on supported mobile devices, such as the address bar and surrounding browser chrome.
The pages were structured using HTML5 semantic landmarks:
headerwithrole="banner"navwithrole="navigation"andaria-label="Primary"- Skip link as the first interactive element
mainwithid="main-content"- Named
sectionelements usingaria-labelledby asidefor Quick Statsfooterwithrole="contentinfo"
Semantic landmarks help screen readers understand the page structure and allow users to navigate quickly between important regions.
A <section> becomes a landmark only when it has an accessible name, usually through aria-labelledby or aria-label.
The skip link lets keyboard and screen reader users jump directly to the main content without tabbing through the navigation on every page.
The filter area was built using:
<form role="search" method="get">- Search input (
type="search") - Business Unit dropdown
- Status dropdown
- Submit button
- Proper
<label for="">for every control inputmode="search"autocomplete="off"- Live region using
aria-live="polite"for future result updates
Using role="search" identifies the form as a search region for assistive technologies. Proper labels improve accessibility, while the live region will allow JavaScript to announce filter results without interrupting the user.
The results were displayed using a semantic HTML table containing:
- Table caption
- Column headers with
scope="col" aria-sort="none"on sortable columns- Eight sample rows
- Row headers using
scope="row" - Status displayed as text ("Active" / "Inactive")
- Edit action as a link
- Delete action as a POST form with a submit button
Using semantic table elements makes tabular data easier for screen readers to understand. Text-based status ensures accessibility, and using a POST form for Delete follows proper web semantics.
Deleting data changes application state, so it should use POST rather than GET. GET requests are intended only for retrieving data and can be triggered accidentally by bookmarks, crawlers, or browser prefetching.
The Add and Edit pages use a semantic HTML form with:
<form method="post" action="#" novalidate aria-labelledby="form-title"><fieldset>and<legend>to group related fields- Required fields:
- Attribute Name
- Business Unit
- Customer Location
- Company
- Status (radio group)
- Created On
- Notes
- Appropriate input types (
text,select,radio,date,textarea) - Validation attributes (
required,minlength,maxlength,pattern) - Proper
<label for="">for every field - Hint text connected using
aria-describedby - Empty error placeholders with
role="alert"andaria-live="polite" - Error summary region at the top of
<main>for future JavaScript validation
The form is structured to be accessible and ready for future CSS and JavaScript. All fields have unique IDs, labels, hints, and error placeholders so custom validation can be added without changing the HTML.
novalidate disables the browser's default validation popups while keeping all HTML validation rules. This allows JavaScript in A3 to display consistent custom error messages using the existing validation attributes.
Browsers tested:
- Google Chrome
- Microsoft Edge
Observation:
When the form had novalidate, the browser allowed submission with required fields empty.
Result:
- No native browser validation messages were displayed.
- HTML validation was disabled by the browser.
After removing novalidate, the form was submitted again with empty required fields.
Result:
-
Google Chrome showed similar validation prompts.
After testing, the novalidate attribute was restored.
Built-in browser validation using attributes such as:
requiredminlengthmaxlengthpatterntype
This provides quick client-side feedback.
JavaScript enhances validation by:
- displaying custom error messages
- highlighting invalid fields
- showing an error summary
- enforcing additional rules
Server-side validation is mandatory because the client can be bypassed.
Users can bypass browser validation by:
- using developer tools
- removing HTML validation attributes
- disabling JavaScript
- sending requests with
curlor Postman
Only server-side validation can be trusted in production.
- Layout: Header → filter section → results table → quick stats → footer
- Rejected: sidebar filters and extra table columns like Created On / Notes
- Above the fold: navigation, filters, and the start of the results table
- Mobile-first: stack filters vertically, move Quick Stats below the table, and use a responsive table layout
- Layout: single-column form with grouped fields and bottom action buttons
- Rejected: two-column form and top action buttons
- Above the fold: page title, error summary, and first required fields
- Mobile-first: full-width inputs and buttons with comfortable spacing
- Layout: consistent structure with Add page, plus metadata before the form
- Rejected: metadata at the bottom and a side panel
- Above the fold: page title, record details, and the start of the form
- Mobile-first: keep the single-column layout and stack metadata above the form
- Skip to main content
- Attribute List
- Add Attribute
- Search
- Business Unit
- Status
- Apply Filters
- Edit (row 1)
- Delete (row 1)
- Edit (row 2)
- Delete (row 2) ...
- Delete (last row)
- Skip to main content
- Attribute List
- Add Attribute
- Attribute Name
- Business Unit
- Customer Location
- Company
- Status (Active selected; use arrow keys to switch)
- Created On
- Notes
- Save Attribute
- Reset
- Skip to main content
- Attribute List
- Add Attribute
- Attribute Name
- Business Unit
- Customer Location
- Company
- Status (Active selected; use arrow keys to switch)
- Created On
- Notes
- Update Attribute
- Reset
Lighthouse reported the warning "Touch targets do not have sufficient size or spacing", which resulted in an Accessibility score of 96/100.
Cause: The navigation links were displayed close together, so their clickable (touch) areas did not meet the recommended minimum size and spacing for touch devices. Lighthouse evaluates the rendered page, not just the HTML, so even though the navigation used the correct semantic structure (<nav><ul><li><a>), the default browser styling left the links too close together.
Observation: I tested replacing the semantic list (<ul><li>) with direct <a> elements inside the <nav>, and Lighthouse then reported an Accessibility score of 100/100. However, this changed the page layout rather than addressing the actual issue.
Decision: I kept the semantic list structure because navigation menus should be marked up as a list of links. The warning is related to presentation rather than HTML semantics and will be resolved in the CSS phase by increasing the padding and spacing between the navigation links.
- File Architecture: Structured stylesheets into a modular directory (
css/tokens.css,css/reset.css,css/base.css,css/layout.css,css/utilities.css,css/pages.css, andcss/components/*.css) imported intocss/main.css. - Cascade Layers: Declared layer priority
@layer reset, tokens, base, layout, components, utilities, pages;.
Cascade layers decouple priority from selector specificity (a,b,c) and file loading order. Without @layer, if a base reset or utility stylesheet accidentally has a higher specificity selector (such as an ID or chained classes), overriding it in a later component file requires artificially increasing specificity or resorting to !important. With @layer, any CSS rule defined in a higher-priority layer (components) automatically wins over rules in lower-priority layers (reset or base), even if the lower layer has higher selector specificity.
In the CSS cascade specification, unlayered CSS (styles not inside any @layer block) automatically has higher priority than ALL styles defined inside any @layer. If a developer builds their application entirely inside @layer blocks and later a third-party script or developer injects normal unlayered CSS, those unlayered rules immediately override all layered rules regardless of specificity. Conversely, if an external developer tries to override existing unlayered CSS by placing their custom code inside a @layer, their overrides will fail because any unlayered rule always defeats layered rules.
Plain link order is preferable in simpler projects without complex overrides, or when integrating with legacy codebases and third-party libraries (like Bootstrap or old UI widgets) that rely heavily on selector specificity and unlayered cascades. Wrapping legacy frameworks inside @layer can unexpectedly invert specificity hierarchy and break their built-in overrides.
- Tokens Implemented: Defined centralized CSS custom properties on
:rootfor color primitives (--color-bg,--color-primary), semantic feedback (--color-danger,--color-success,--color-focus-ring), fluid spacing (--space-1to--space-6inrem), fluid typography (clamp()on--fs-xland--fs-2xl), border radii (--radius-sm,--radius-md,--radius-pill), shadows (--shadow-1,--shadow-2), and stacking (--z-toast,--z-skip-link). - Dark Mode Strategy: Implemented dark mode theme overrides using
[data-theme="dark"]. By changing semantic color variables under this attribute selector, the application switches themes dynamically without modifying component stylesheets.
CSS custom properties (--color-bg) exist live in the browser's DOM tree at runtime. When the theme attribute ([data-theme="dark"]) or system preference (prefers-color-scheme: dark) changes, the browser instantly re-evaluates and repaints elements using those variables. Sass variables ($color-bg) are preprocessed and compiled away into static hex or rgb strings (#ffffff) at build time. To switch themes using Sass variables, a developer would have to duplicate every single component selector (.dark-theme .card { background: #121212; }), doubling or tripling CSS payload size and requiring full class toggling across all components.
Sass variables are superior when performing compile-time calculations, loops (@for), map data structures, or static mathematical and color manipulation functions (darken($base, 10%), lighten(), color.adjust()). If values are static constants across all themes and devices, Sass variables avoid adding hundreds of custom property lookups to the browser's runtime CSS Object Model (CSSOM).
- Modern Reset (
reset.css): Applied globalbox-sizing: border-boxacross all elements, normalized heading and paragraph margins (margin: 0), made responsive images default (max-width: 100%; display: block;), and standardized form control typography (font: inherit).
To maintain long-term maintainability without !important, all components follow the BEM (Block__Element--Modifier) naming convention (e.g., .attribute-card, .attribute-card__title, .attribute-card--inactive). BEM enforces flat specificity, where every component style has an identical specificity score of exactly one class (0,1,0). Without a strict convention like BEM, developers often chain selectors (.card .content h3.active), causing specificity inflation ((0,2,1)). When another developer needs to override that style later, they must chain even more selectors or use !important, setting off a cascading specificity war where stylesheets grow unmaintainable and unpredictable.
- Implemented Rules: Added logical properties (
margin-inline,padding-block,margin-block-end) for internationalization (i18n / RTL readiness),:target { scroll-margin-top: var(--space-5); }for anchor link jumps above sticky headers,.sr-onlyaccessibility utility, and::selectionbackground styling.
Removing the default focus outline (outline: none) without providing a visible focus indicator violates WCAG 2.1 Criterion 2.4.7 (Focus Visible). Keyboard-only users and screen-reader users relying on keyboard tabbing (Tab / Shift+Tab) cannot see which interactive element currently has focus on the webpage. Without a visible ring, navigation becomes completely blind and unusable.
The traditional :focus pseudo-class triggers whenever an element receives focus, including when a mouse user clicks a button (<button>). Many designers historically removed :focus outlines because mouse users complained about unsightly blue rings after clicking buttons. :focus-visible discriminates between input modalities: the browser uses heuristics to show the focus ring only when the user is navigating via keyboard (Tab keys) or assistive devices, while hiding it when a mouse or touch user clicks an element. This satisfies both visual aesthetics and WCAG accessibility compliance.
-
Implemented Rules: Built responsive page chrome using CSS Grid (
grid-template-areas) with mobile-firstem-based breakpoints (48emfor tablet$\ge 768px$ and64emfor desktop$\ge 1024px$ ).
Mobile-first styling sets single-column flow as the natural default (body { display: grid; grid-template-columns: minmax(0, 1fr); }) without requiring a media query. This avoids the classic "desktop-first bug" where multi-column grid templates, explicit column widths, and large desktop margins must be aggressively overridden or reset (width: 100%, grid-template-columns: 1fr) on small screens. When overriding complex desktop layouts for mobile, developers frequently encounter specificity wars and overflow bugs that cause horizontal scrollbars on mobile phones. Mobile-first ensures small screens only process baseline styling while larger screens opt into multi-column complexity via min-width media queries.
Media queries defined in em units (min-width: 48em) respect the user's browser font-size scaling preferences. If a user with low vision increases their browser's default font size from 16px to 24px, a pixel breakpoint (min-width: 768px) remains static at 768px, causing large text to cram into narrow layout columns or overflow boundaries. An em breakpoint (48em = 48 * 16px = 768px) dynamically scales up with the user's base font size (48 * 24px = 1152px), ensuring the layout only shifts to a multi-column grid when there is genuine physical screen space to accommodate 48 characters of their zoomed-in text.
For page-level structural layouts ("page chrome"), grid-template-areas provides a self-documenting ASCII visual map of the exact document layout directly in CSS ("header header header" / "nav main aside" / "footer footer footer"). Using chains of numeric grid line coordinates (grid-column: 1 / 4; grid-row: 2 / 3;) makes layout relationships opaque and prone to breaking during refactoring. With grid-template-areas, moving a sidebar from the right column to the left requires editing only a single string matrix without modifying the individual component styles.
- Implemented Rules: Added
container-type: inline-size; container-name: attribute-list;to the table/card wrapper (tbody/#results-section). Inside@container attribute-list (max-width: 320px), the card collapses down to a compact 1-line summary displaying only the Attribute Name header and Status/Action badges (td:not(:last-child):not(:nth-last-child(2)) { display: none; }).
A Media Query (@media (max-width: 48em)) only inspects the total physical width of the browser window viewport. If a component is placed inside a narrow sidebar (280px wide) while the desktop browser window itself is wide (1920px), a Media Query will fail to trigger, forcing the component to stay in wide/multi-column mode and overflow its container.
Component Example where @media fails: An Attribute Card (.attribute-card) placed inside the right-hand Quick Stats sidebar (#quick-stats) vs the Main Column (#main-content). In a wide browser (1400px), the main column has 900px of space while the sidebar only has 280px. A Media Query cannot style the sidebar card differently from the main column card because the viewport width is identical for both (1400px). A Container Query (@container (max-width: 320px)) measures the parent box's width (280px), cleanly collapsing the sidebar card into a compact 1-line layout while letting the main column card display in full detail.
- Implemented Rules: Styled the desktop
<table>with zebra striping (tbody tr:nth-child(even)), hover highlighting, sticky column headers (th { position: sticky; top: 0; }), right-alignment for Actions, focus-visible indication on rows (tbody tr:focus-within), and sort arrows driven purely by[aria-sort]selectors via::after. On mobile (< 48em), transformed<tr>elements into elevated cards using pure CSS (display: block) anddata-labelinjection (td::before { content: attr(data-label); }).
Maintaining two separate HTML blocks (<table class="desktop-only"> alongside <div class="mobile-cards">) forces developers to duplicate every piece of data in the DOM (100% markup duplication). When data changes dynamically or through pagination, both copies must be kept synchronized, which increases memory footprint and introduces bugs where one view updates while the other does not. Furthermore, having duplicate interactive buttons (edit/delete forms inside both desktop table cells and mobile card divs) creates duplicate tab stops for keyboard users and confusing duplicate announcements for screen readers. Using data-label="..." on table cells combined with td::before { content: attr(data-label); } preserves a single, canonical DOM table (100% DRY architecture) while allowing CSS to completely restructure the visual layout between columns and cards.
Applying display: block; or display: flex; to semantic table elements (<table>, <thead>, <tbody>, <tr>, <td>) strips away native table semantics in certain browsers and assistive technologies (e.g., Apple VoiceOver on Safari or older NVDA/JAWS versions). When CSS overrides the table display properties, screen readers may stop announcing column relationships, row counts ("Table with 8 rows and 6 columns"), and header-to-cell associations (headers="attr-color"). Furthermore, some screen readers announce ::before generated content (content: attr(data-label)) as plain string literals directly before the cell data, or in rare cases announce cell contents twice. To mitigate semantic loss when converting tables to cards via CSS, developers must often explicitly re-assert ARIA roles (role="table", role="rowgroup", role="row", role="cell") on the elements if tabular navigation needs to be preserved for screen reader users on mobile devices.
During the implementation and verification of Task 7, we solved three critical, real-world CSS rendering gotchas:
- The
overflow: hidden/overflow-x: autoSticky Trap: Whenposition: sticky; top: 0;is applied to<th>elements, it attaches exclusively to the immediate scrolling ancestor box. If any intermediate parent container (table,#results-section) hasoverflow: hiddenoroverflow-x: auto, the browser locks the sticky coordinate system to that inner box. When the user scrolls the main browser window (windowscrollbar), the inner box is not scrolling vertically, causing the sticky headers to scroll right off the screen. The Fix: Removed alloverflowclipping fromtableand#results-section, allowingposition: stickyto attach 100% directly to the browser window viewport. - The
thead thvstbody th[scope="row"]Stacking Trap: Initially, applyingth { position: sticky; top: 0; }caused every<th>element across the entire table to stick during scrolling. In semantic tables, data rows often use<th scope="row">for their first cell (Color,Size,Material). Because genericthapplied sticky rules to both the header titles AND the row titles, scrolling down caused thetbody th[scope="row"]cells (Color,Size) to slide up totop: 0and stick directly over theAttribute Nameheader cell, making it appear to disappear. The Fix: Strictly scoped the sticky rule tothead th { position: sticky; top: 0; z-index: var(--z-elevated); }. - The
display: blockvsdisplay: table-captionWidth Bug: When an HTML<caption>element is forced todisplay: block; width: 100%;inside a<table>(display: table), some browser layout engines compute the100%block width against the very first table column (~250px) rather than the combined width of all columns. The Fix: Keptdisplay: table-caption; caption-side: top;oncaptionso the table layout engine calculates100%across the entire multi-column width.
- Implemented Rules: Styled
fieldset,legend,.form-group, and.form-fieldwith elevated spacing and grouping. Enforcedmin-height: 44pxacross all inputs,<select>, and<textarea>elements to satisfy WCAG 2.5.8 Minimum Touch Target requirements. Added high-contrast:focus-visiblerings (outline: 3px solid var(--color-focus-ring)) with offset. Styled validation feedback using.form-hint,.form-error, and dynamic error states (input:user-invalid,.form-group--error). Custom-styled<select>dropdown arrows with inline SVG background images that adapt cleanly to dark mode ([data-theme="dark"]).
Why should placeholder text NEVER be used as a replacement for a visible <label> element? What WCAG and UX failures occur when labels are omitted?
Omitting visible <label> elements in favor of placeholder text causes critical usability and accessibility failures across modern web forms:
-
UX Failure ("The Vanishing Prompt"): As soon as a user focuses an input and begins typing their first character, the placeholder text vanishes completely. If the user gets distracted or needs to review a long form before submitting, they have no visual indication of what data was requested by each field (
cognitive load failure). Furthermore, placeholders cannot be selected or copied by users. -
WCAG Accessibility & Screen Reader Failure: Assistive technologies (screen readers like NVDA, JAWS, and VoiceOver) often treat
placeholderattributes inconsistently or skip announcing them entirely when navigating between form controls. Without a<label for="...">or explicitaria-labelledbyassociation, blind users land on anonymous input boxes and hear only"Edit text, blank". -
WCAG Contrast Failure (SC 1.4.3): By default across browsers, placeholder text renders as low-contrast light gray (
#999999on#ffffff$\approx 2.8:1$ ). This violates WCAG Success Criterion 1.4.3 (Contrast Minimum), which mandates a minimum contrast ratio of4.5:1for readable text. If a developer darkens the placeholder to pass4.5:1contrast, users frequently mistake the field for already containing pre-filled text (false affordance) and skip filling it out entirely. Therefore, a permanent, high-contrast, visible<label>element must always accompany every form field.
The CSS relational/parent selector :has() enables declarative logic directly inside stylesheets that previously required DOM queries (closest(), querySelector()) and event listeners (addEventListener):
- Parent/Ancestor Styling on Child Focus: Styling an outer
.form-groupcard or border whenever an inner<input>receives focus (.form-group:has(input:focus-visible) { border-color: var(--color-primary); }). Previously, developers had to attach JSonfocus/onblurhandlers to add/remove classes on the parent wrapper. - Conditional Sibling Styling based on Descendant Attributes: Reaching up from
<input required>to the parent wrapper (.form-group) and then styling a different sibling element inside that group, such as automatically appending a red*asterisk to<label>(.form-group:has(input[required]) label::after { content: " *"; color: var(--color-danger); }). Previously, this required JS DOM traversal on page load. - DOM Quantity & Content Queries: Checking if an element exists inside a tree or if a specific item count is reached (
body:has(dialog[open]) { overflow: hidden; }to lock background scrolling when a modal opens, orul:has(li:nth-child(6)) { display: grid; }to change layouts when items exceed 5).
If a developer applies error styling using :invalid (input:invalid { border-color: red; }), every required field across a brand new, untouched form immediately glows red before the user has even touched their mouse or keyboard (premature validation). This aggressive visual hostility increases user anxiety and abandonment rates. :user-invalid is a modern CSS pseudo-class that only triggers after the user has interacted with the control (e.g., focused the field, typed something, and tabbed away blur, or attempted to submit the form). This ensures users receive polite, respectful validation feedback that only alerts them after they have actually made an error.
We upgraded our form layout inside forms.css to use a Mobile-First CSS Grid Two-Column Architecture:
- Mobile Default (
< 48em):fieldset { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2) var(--space-5); }ensures all form fields stack in a single vertical column for comfortable thumb reach. - Desktop Grid (
≥ 48em / 768px):@media (min-width: 48em) { fieldset { grid-template-columns: repeat(2, minmax(0, 1fr)); } }shifts form inputs into a balanced 2-column grid (Attribute Namenext toBusiness Unit,Customer Locationnext toCompany). - Auto-Spanning (
grid-column: 1 / -1): Using:has()selectors (fieldset .form-group:has(textarea),fieldset .form-group:has(fieldset)), multi-line textareas and nested status groups automatically span across both grid columns without requiring manual HTML utility classes!
To provide clear visual guidance when a form section contains validation errors, we implemented parent validation styling using :has():
- Rule Implementation:
fieldset:has(input:user-invalid), fieldset:has(select:user-invalid), fieldset:has(textarea:user-invalid)applies a distinct4pxred left accent stripe and subtle border glow (var(--color-danger)) to the outer<fieldset>wrapper whenever any child input fails validation. This directs user attention immediately to the container containing errors. - Graceful Fallback & Progressive Enhancement: Modern browsers natively execute
:has(). For older Safari versions (< 16.4) where:has()is unsupported, we include.fieldset--invalidand.fieldset--errorutility classes alongside the selector. Even when:has()is unsupported and JS has not added the utility class, older browsers gracefully fall back to styling the individual invalid controls inside (input:user-invalid) without breaking page layout or functionality (100% Progressive Enhancement).
When a developer writes transition: all 0.3s ease;, the browser attempts to interpolate and calculate intermediate values for every single CSS property that changes (including heavy layout properties like width, height, margin, padding, top, left, box-shadow, and border-width). Whenever a layout property changes during an animation, the browser engine must pause, recalculate the geometry of the entire DOM tree (Reflow / Layout), and repaint pixels across the screen (Paint) on every single frame (60 times per second). This causes severe CPU stuttering, frame drops (jank), and battery drain on mobile devices.
The two cheapest properties to animate in CSS are transform (e.g., translateY(), scale(), rotate()) and opacity.
They are cheap because transform and opacity do not trigger DOM reflow or repainting! Instead, the browser offloads them directly to the GPU's Compositor Thread. The GPU simply takes the existing pre-rendered texture layer of the element and moves or fades it across the screen independently of the main JavaScript/DOM thread, guaranteeing buttery-smooth 60fps / 120fps performance.
@media (prefers-reduced-motion: reduce) detects whether a user has enabled "Reduce Motion" or "Remove Animations" inside their operating system accessibility settings (Windows, macOS, iOS, or Android).
- The WCAG Criteria Behind It: It honors WCAG Success Criterion 2.3.3 (Animation from Interactions - Level AAA) and WCAG 2.2.2 (Pause, Stop, Hide - Level A).
- The Medical Rationale: Many users suffer from vestibular disorders, inner ear conditions, epilepsy, or chronic migraines. When UI elements rapidly slide, zoom, or bounce across the screen, it can trigger physical dizziness, vertigo, nausea, or seizures. Therefore, wrapping all transitions in
@media (prefers-reduced-motion: no-preference)and providing zeroed fallbacks (transform: none) is a vital accessibility mandate.
CSS specificity determines which selector wins when multiple rules target the same element. It is represented as a four-part value:
| Component | Represents | Example |
|---|---|---|
| a | Inline styles | style="color: red;" |
| b | ID selectors | #submit-btn |
| c | Classes, attributes, and pseudo-classes | .btn, [disabled], :hover, :is() |
| d | Element selectors and pseudo-elements | button, h1, ::before |
To understand how the CSS cascade determines the final applied style, the following rules were added in the given order:
.btn {
background: blue;
}
button.btn {
background: green;
}
#submit-btn {
background: red;
}
:where(.btn) {
background: pink;
}
:is(.btn) {
background: teal;
}
.btn {
background: orange !important;
}The HTML used for testing was:
<button
id="submit-btn"
class="btn"
style="background: yellow;">
Apply Filters
</button>| CSS Rule | Specificity (a,b,c,d) | Notes |
|---|---|---|
.btn |
(0,0,1,0) | One class selector |
button.btn |
(0,0,1,1) | One element + one class |
#submit-btn |
(0,1,0,0) | One ID selector |
:where(.btn) |
(0,0,0,0) | :where() always has zero specificity |
:is(.btn) |
(0,0,1,0) | Takes the specificity of .btn |
.btn { background: orange !important; } |
(0,0,1,0) + !important |
!important changes declaration priority, not specificity |
style="background: yellow;" |
(1,0,0,0) | Inline style |
style="background: yellow !important;" |
(1,0,0,0) + !important |
Inline style with highest author priority |
style="background: yellow;"Predicted colour: Orange
Verified colour: Orange ✅
Reason:
- The inline style is a normal declaration.
- The stylesheet rule uses
!important. !importantdeclarations override all normal author declarations, including inline styles.
style="background: yellow !important;"Predicted colour: Yellow
Reason:
- Both declarations are marked
!important. - When importance is equal, the browser compares specificity.
- The inline style has specificity (1,0,0,0), which is higher than
.btn(0,0,1,0). - Therefore, the inline
!importantstyle wins.
The :where() pseudo-class always has zero specificity (0,0,0,0), regardless of the selector inside it.
This makes it ideal for writing base styles that should remain easy to override later.
The :is() pseudo-class takes the highest specificity of the selectors passed into it.
Example:
:is(.btn)Since .btn has specificity (0,0,1,0), :is(.btn) also has specificity (0,0,1,0).
An inline !important declaration combines:
- Inline specificity (1,0,0,0)
!importantpriority
This makes it one of the strongest author-origin declarations.
It can only be overridden by another applicable !important declaration with higher precedence in the CSS cascade (for example, certain user-origin !important styles used for accessibility).
After completing this exercise, my takeaway is:
Avoid using
!importantin production code whenever possible. A well-structured CSS architecture with proper cascade order and low-specificity selectors is easier to maintain.!importantshould be reserved for exceptional situations, such as utility classes or overriding unavoidable third-party CSS.
The Styles panel displays every CSS rule that matches the selected element. It also shows the complete CSS cascade, allowing me to see which rule is applied and which rules are overridden.
For this exercise, I inspected the Apply Filters button used in Task 10.
The inline style normally has the highest specificity (1,0,0,0), but it is a normal declaration. The stylesheet rule using !important takes precedence over all normal declarations, including the inline style, resulting in the button being rendered with an orange background.
I inspected the "Filter Attributes" (<h2>) heading.
The Styles panel showed the original CSS rule:
The Computed panel showed the final resolved value:
margin-block-start: 0pxmargin-block-end: 12px
This demonstrates that the Computed panel resolves CSS variables and displays the actual values used by the browser after the CSS cascade has been applied.
Difference:
- Styles: Shows all matching CSS rules and where they come from.
- Computed: Shows only the final value applied to the element.
The Layout panel in Chrome DevTools was used to visualize the page layout and inspect both CSS Grid and Flexbox.
- Grid Overlay: Inspected the
bodyelement, which uses CSS Grid. The overlay displayed the grid structure, including rows, columns, and grid gaps, making it easier to understand the overall page layout. - Flex Overlay: Inspected the
#main-contentelement, which uses Flexbox (display: flex; flex-direction: column;). The overlay showed the vertical main axis and how child elements are stacked and spaced within the container.
These overlays are useful for debugging layouts and verifying that Grid and Flexbox behave as expected.
The Coverage tab was used to measure how much of the project's CSS was executed while interacting with the page.
I only covered one page sp , the report showed that several CSS files had unused rules during the recording. For example:

Using the Rendering panel, I enabled Emulate CSS prefers-reduced-motion to verify that motion-sensitive users receive a reduced-motion experience.
The page correctly respected the user's motion preference by disabling transitions, confirming that the reduced-motion media query was working as expected.
A Lighthouse audit was run on all three pages using the following categories:
All pages achieved the target scores.
This project uses ES Modules (<script type="module">) to organize JavaScript into separate, reusable files using import and export.
Advantages
- Better code organization and maintainability.
- Modules run in strict mode automatically.
- Supports modern JavaScript features such as top-level
await. - Module scripts are deferred by default.
Disadvantages
- Cannot be reliably run using the
file://protocol. - Requires a local web server during development.
- Older browsers may need additional support.
Module scripts are automatically deferred, meaning the browser downloads them while parsing HTML but waits until the document has been fully parsed before executing them.
As a result:
- DOM elements are usually available when the module runs.
- In many cases, a
DOMContentLoadedlistener is not required. DOMContentLoadedfires only after module scripts have finished executing.
Opening the application directly from the filesystem (for example, file:///C:/project/index.html) can cause module imports to fail because browsers apply security and CORS restrictions to ES module loading.
This often results in errors such as:
- "Failed to load module script"
- CORS-related import errors
VS Code Live Server serves the project over HTTP (for example, http://localhost:5500) instead of file://.
Benefits include:
- ES module imports work correctly.
- No
file://CORS issues. - Automatic browser refresh during development.
- Behavior closer to a real production environment.
Copy this directly into your README.md:
localStorage is shared by all applications running on the same origin.
If two applications use the same key name, such as "theme", they can overwrite each other's stored values.
Example:
Application A:
localStorage.setItem("theme", "dark");Application B:
localStorage.setItem("theme", "light");Now the value stored by Application A is overwritten by Application B.
To avoid this issue, this project uses namespaced keys:
ams.attributes
ams.businessUnits
ams.locations
ams.companies
ams.theme
ams.seedVersion
The ams prefix ensures that Attribute Management System data remains isolated and prevents key collisions with other applications.
localStorage operations can fail in several situations:
- Stored JSON data becomes corrupted.
JSON.parse()throws an error when invalid JSON is encountered.- Private browsing modes may restrict storage access or provide zero storage quota.
- Storage limits can be exceeded, causing
QuotaExceededError.
Using try/catch prevents the application from crashing and allows the application to return safe fallback values when storage operations fail.
innerHTML is acceptable when rendering trusted, static markup that is fully controlled by the application.
innerHTML is dangerous when inserting user-supplied or untrusted content because it can execute malicious HTML or JavaScript.
Cross-Site Scripting (XSS) occurs when an attacker injects executable script into a page that is then run in another user's browser.
Appending elements one by one can trigger repeated DOM updates and layout calculations. A DocumentFragment allows many nodes to be built in memory first and inserted into the DOM in a single operation, reducing reflow and repaint work.
I implemented filtering on the Attribute List page using a debounced search input and dropdown filters for Business Unit and Status. The search logic uses JavaScript array methods such as filter(), includes(), and toLowerCase() instead of loops.
The debounce function delays execution of the search until the user stops typing for a specified time. It uses setTimeout() together with a closure variable (timeoutId).
When the user types:
- Any existing timer is cancelled using
clearTimeout(). - A new timer is created.
- The callback executes only after the user stops typing for the configured delay.
This prevents unnecessary filtering and rendering on every keystroke, improving performance and user experience.
The current filter state is mirrored into the URL using history.replaceState(). For example:
index.html?search=invoice&businessUnit=bu-1&status=active
On page load, the application reads these values from the URL and restores the filters automatically.
Without storing filter state in the URL, refreshing the page would clear the visible filter selections and create confusion because users would lose their current view. Keeping filters in the URL also allows bookmarking, sharing links, and restoring state after refresh.
Event delegation is a JavaScript technique where a single event listener is attached to a parent element to handle events triggered by its child elements. It works using event bubbling, where the event moves from the target element up to its parent.
-
Better performance
- Fewer event listeners are created, which reduces memory usage and improves efficiency.
-
Supports dynamic elements
- Newly added child elements automatically work with the same parent listener without needing separate event listeners.
Event delegation does not work for events that do not bubble, such as:
focusblurmouseentermouseleave
For these cases, use bubbling alternatives:
focusinfocusout
In a real-world enterprise application, data sets can grow exponentially. If a database contains millions of records, paginating on the server ensures that only a tiny, requested slice (e.g., 5 or 20 rows) is retrieved from the database, serialized, transmitted over the network, and loaded into the browser's memory at any given time. This optimizes bandwidth, minimizes server CPU/memory usage, and dramatically speeds up the client application.
If an application attempts to load 100,000 rows at once to perform pagination purely on the client-side:
- Network Payload: Transferring a massive JSON payload blocks the network and causes slow loading times.
- Memory Leaks & Crashes: The browser tab consumes excessive RAM to parse and hold the JSON objects, leading to out-of-memory crashes on mobile devices or lower-end machines.
- Layout Thrashing & UI Freezes: Any filtering, sorting, or DOM manipulation over 100k rows blocks the JavaScript main thread, making the UI completely unresponsive.
- Stale Data: By the time the user reaches page 50 of their 100k rows, the data may already be outdated compared to the server's state.
To migrate to server-side pagination, the client and server must agree on an API contract:
- Request (Client to Server): The client sends query parameters specifying the page state, such as
?page=2&limit=5&sortBy=attributeName&sortDir=desc. - Response (Server to Client): The server returns a standardized JSON payload containing the requested slice of data and pagination metadata. For example:
{ "data": [ ...5 rows... ], "meta": { "totalItems": 1500, "totalPages": 300, "currentPage": 2 } }
The client relies on meta.totalItems to generate the correct number of pagination buttons without needing the full dataset.
setTimeout schedules a function to run exactly once after a specified delay, while setInterval schedules a function to run repeatedly at a regular interval. You must use clearTimeout (or clearInterval) when a scheduled task is no longer needed—for example, if a user manually closes a notification before its auto-dismiss timer fires, preventing the callback from executing on an element that no longer exists.
If a global single-toast manager stores its timer in a single variable, rapidly triggering multiple toasts will overwrite that variable, leaving the previous timeouts orphaned and active. The stale timers will then fire prematurely, unexpectedly hiding the newly rendered toast. By using a Map that associates each unique toast DOM element with its specific timer ID, we can independently track and clear the correct timer whenever a toast is closed manually or superseded.
When translating the dependent-dropdown pattern to a real backend, developers must choose between eager-loading and lazy-loading:
- Eager-load (All at once): The client fetches all business units and all locations in one large payload on initial load, doing the filtering in browser memory (as we did here). This uses fewer network requests (better for high-latency connections) but results in a larger initial payload, which could be slow if there are tens of thousands of locations.
- Fetch per change (Lazy-load): The client fetches only the business units initially. When the user selects a BU, the client makes a targeted API request (e.g.,
GET /api/locations?businessUnitId=BU001). This keeps the initial payload small and memory usage low, but it introduces a slight network delay (latency) every time the user changes the business unit drop-down.
HTML5 provides built-in validation attributes (like required, pattern, minlength), but uniqueness validation is impossible in HTML alone because HTML has no concept of application state or the ability to query a database/list of existing records to compare against.
Even though the UI restricts the location dropdown to valid locations, a malicious user can easily use browser Developer Tools to manually change the value of an <option> element before submitting. Server-side (and JavaScript) validation must verify the logical relationship (defense in depth) to prevent corrupted data from entering the database.
Setting focus to the error summary on submit failure is a critical WCAG accessibility concern. Without it, screen reader users who click submit would hear silence (or whatever element naturally falls next), completely unaware that the submission failed or that error messages appeared elsewhere on the page. Moving focus ensures the errors are immediately announced.
| Our Manual State | Angular Reactive Forms Equivalent | Description |
|---|---|---|
!touchedFields.has(field) |
ng-untouched |
Field has never lost focus. |
touchedFields.has(field) |
ng-touched |
Field has lost focus at least once (blur event fired). |
value !== "" (implied via our input listener) |
ng-dirty |
The user has changed the value in the UI. |
By building this by hand, we learn that Angular's Reactive Forms gives us state tracking (touched, dirty, pristine, valid, invalid) entirely for free, bound directly to the HTML template without needing manual event listeners. However, it does NOT give you the actual business logic (like "Attribute Name must be unique within a BU"); you still have to write those custom validator functions yourself.
Promise.all uses a "fail-fast" behavior. If even one of our 4 JSON files fails to download, the entire Promise immediately rejects and throws an error. I picked Promise.all for this task because our app is useless without all 4 files. If locations.json fails to load, the user can't fill out the form properly, so we want the app to fail-fast and show an error banner rather than loading a broken experience.
Promise.allSettled uses a "collect-all" behavior. It waits for every single request to finish, even if some of them fail, and gives you a list of the successes and failures. This would be bad for our app, because we don't want to load half a database.
Promise.any solves a completely different problem: "race to the finish." It fires off multiple requests and immediately resolves the millisecond the first one succeeds, ignoring the rest.
Why is debounce alone not enough? Using a debounce prevents sending an excessive number of requests, but it does not solve the "Stale-Response-Wins" bug. If a slow request is fired, followed immediately by a fast request, the slow request might resolve after the fast one, causing its outdated data to overwrite the UI.
How does AbortController fix this?
An AbortController fixes this by acting as a kill switch. Before we fire a new search request, we call abort() on the previous request's controller. This guarantees that any outdated requests are instantly cancelled and throw an AbortError, ensuring only the absolute latest request can ever reach the UI.
When else would you use AbortController in vanilla JS?
It is incredibly useful for cleanly removing event listeners without needing to keep a reference to the original function (by passing { signal: controller.signal } to addEventListener), aborting ReadableStream operations, and cancelling fetch() requests.
By design, the native fetch() API only rejects a Promise if there is a network failure (like the user losing WiFi). It resolves successfully even if the server returns a 404 (Not Found) or 500 (Internal Server Error) status code. Because of this, a simple try/catch block will miss these HTTP errors entirely. To handle both, we must check response.ok inside the try block and manually throw new Error() if it is false. Third-party libraries like axios differ from this because they automatically reject Promises for any status code outside the 200-299 range.
Why prefer the user's explicit choice over the OS preference?
A user's operating system might be set to Dark Mode, but they might specifically prefer reading our application in Light Mode. If we constantly force the OS preference on every page load, we remove their autonomy and create a frustrating experience. Once a user makes an explicit choice (saved in localStorage), we must respect it as the highest priority.
What is the CSS-only equivalent, and why is JS better?
The CSS-only equivalent is using the @media (prefers-color-scheme: dark) media query to automatically apply dark styles. While CSS-only is faster and requires no JavaScript, it is completely rigid. It offers zero override capability. If a user wants their OS in dark mode but our app in light mode, CSS-only makes that impossible. The JavaScript toggle gives us the best of both worlds: a smart OS fallback with full user control.
The application was audited using Google Chrome's built-in Lighthouse tool to ensure high performance, accessibility, best practices, and SEO.
All scores successfully exceeded the assignment target thresholds (≥95 and ≥90).
For the index.html page the issue was regarding the comtrast of color of delete button, hence the color contrast was increased and the accessibility improved.




.png)
.png)








