Skip to content

Commit 55045bb

Browse files
mudcubeclaude
andcommitted
fix: simplify SearchModal to resolve hydration mismatch
Removed the search-container wrapper div and made the search trigger directly position: fixed. This eliminates the hydration mismatch error that occurred when the container wrapper caused DOM structure differences between SSR and CSR. Changes: - Remove .search-container wrapper div and styles - Change .search-trigger from position: relative to position: fixed - Add z-index: 100 to search trigger for proper layering - Remove unnecessary pointer-events manipulation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 49156a3 commit 55045bb

1 file changed

Lines changed: 101 additions & 121 deletions

File tree

src/lib/components/SearchModal.svelte

Lines changed: 101 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -149,135 +149,115 @@
149149
});
150150
</script>
151151

152-
<div class="search-container">
153-
<!-- Trigger Button -->
154-
<button
155-
class="search-trigger"
156-
onclick={openModal}
157-
type="button"
158-
aria-label="Search documentation"
152+
<!-- Trigger Button -->
153+
<button class="search-trigger" onclick={openModal} type="button" aria-label="Search documentation">
154+
<Search size={18} />
155+
<span class="search-trigger-text">Search...</span>
156+
<kbd class="search-trigger-kbd">
157+
<Command size={12} />
158+
<span>K</span>
159+
</kbd>
160+
</button>
161+
162+
<!-- Modal -->
163+
{#if isOpen}
164+
<div class="search-modal-overlay" onclick={closeModal}></div>
165+
<div
166+
class="search-modal"
167+
role="dialog"
168+
aria-modal="true"
169+
aria-labelledby="search-modal-title"
170+
aria-describedby="search-modal-description"
159171
>
160-
<Search size={18} />
161-
<span class="search-trigger-text">Search...</span>
162-
<kbd class="search-trigger-kbd">
163-
<Command size={12} />
164-
<span>K</span>
165-
</kbd>
166-
</button>
167-
168-
<!-- Modal -->
169-
{#if isOpen}
170-
<div class="search-modal-overlay" onclick={closeModal}></div>
171-
<div
172-
class="search-modal"
173-
role="dialog"
174-
aria-modal="true"
175-
aria-labelledby="search-modal-title"
176-
aria-describedby="search-modal-description"
177-
>
178-
<!-- Search Input -->
179-
<div class="search-modal-header">
180-
<Search size={20} class="search-modal-icon" aria-hidden="true" />
181-
<label for="search-modal-input" class="visually-hidden" id="search-modal-title"
182-
>Search documentation</label
183-
>
184-
<input
185-
id="search-modal-input"
186-
bind:value={query}
187-
class="search-modal-input"
188-
type="text"
189-
{placeholder}
190-
autocomplete="off"
191-
spellcheck="false"
192-
aria-describedby="search-modal-description"
193-
/>
194-
<button
195-
class="search-modal-close"
196-
onclick={closeModal}
197-
type="button"
198-
aria-label="Close search"
199-
>
200-
<X size={20} aria-hidden="true" />
201-
</button>
202-
</div>
203-
204-
<!-- Hidden description for screen readers -->
205-
<span id="search-modal-description" class="visually-hidden"
206-
>Use arrow keys to navigate results, enter to select, escape to close</span
172+
<!-- Search Input -->
173+
<div class="search-modal-header">
174+
<Search size={20} class="search-modal-icon" aria-hidden="true" />
175+
<label for="search-modal-input" class="visually-hidden" id="search-modal-title"
176+
>Search documentation</label
177+
>
178+
<input
179+
id="search-modal-input"
180+
bind:value={query}
181+
class="search-modal-input"
182+
type="text"
183+
{placeholder}
184+
autocomplete="off"
185+
spellcheck="false"
186+
aria-describedby="search-modal-description"
187+
/>
188+
<button
189+
class="search-modal-close"
190+
onclick={closeModal}
191+
type="button"
192+
aria-label="Close search"
207193
>
194+
<X size={20} aria-hidden="true" />
195+
</button>
196+
</div>
208197

209-
<!-- Results -->
210-
<div class="search-modal-results" role="region" aria-live="polite" aria-atomic="false">
211-
{#if isLoading}
212-
<div class="search-modal-loading">Loading search index...</div>
213-
{:else if query.trim() && results.length === 0}
214-
<div class="search-modal-empty">
215-
<p>No results found for "{query}"</p>
216-
<p class="search-modal-empty-hint">Try a different search term</p>
217-
</div>
218-
{:else if results.length > 0}
219-
{#each results as result, index (result.id)}
220-
<button
221-
class="search-result {index === selectedIndex ? 'selected' : ''}"
222-
onclick={() => handleResultClick(result)}
223-
type="button"
224-
role="option"
225-
aria-selected={index === selectedIndex}
226-
aria-label="{result.title} in {result.section}"
227-
>
228-
<div class="search-result-content">
229-
<div class="search-result-section">{result.section}</div>
230-
<div class="search-result-title">
231-
{@html highlightMatches(result.title, query)}
232-
</div>
233-
{#if result.match.excerpt}
234-
<div class="search-result-excerpt">
235-
{@html highlightMatches(result.match.excerpt, query)}
236-
</div>
237-
{/if}
198+
<!-- Hidden description for screen readers -->
199+
<span id="search-modal-description" class="visually-hidden"
200+
>Use arrow keys to navigate results, enter to select, escape to close</span
201+
>
202+
203+
<!-- Results -->
204+
<div class="search-modal-results" role="region" aria-live="polite" aria-atomic="false">
205+
{#if isLoading}
206+
<div class="search-modal-loading">Loading search index...</div>
207+
{:else if query.trim() && results.length === 0}
208+
<div class="search-modal-empty">
209+
<p>No results found for "{query}"</p>
210+
<p class="search-modal-empty-hint">Try a different search term</p>
211+
</div>
212+
{:else if results.length > 0}
213+
{#each results as result, index (result.id)}
214+
<button
215+
class="search-result {index === selectedIndex ? 'selected' : ''}"
216+
onclick={() => handleResultClick(result)}
217+
type="button"
218+
role="option"
219+
aria-selected={index === selectedIndex}
220+
aria-label="{result.title} in {result.section}"
221+
>
222+
<div class="search-result-content">
223+
<div class="search-result-section">{result.section}</div>
224+
<div class="search-result-title">
225+
{@html highlightMatches(result.title, query)}
238226
</div>
239-
{#if index === selectedIndex}
240-
<CornerDownLeft size={16} class="search-result-enter-icon" aria-hidden="true" />
227+
{#if result.match.excerpt}
228+
<div class="search-result-excerpt">
229+
{@html highlightMatches(result.match.excerpt, query)}
230+
</div>
241231
{/if}
242-
</button>
243-
{/each}
244-
{/if}
245-
</div>
232+
</div>
233+
{#if index === selectedIndex}
234+
<CornerDownLeft size={16} class="search-result-enter-icon" aria-hidden="true" />
235+
{/if}
236+
</button>
237+
{/each}
238+
{/if}
239+
</div>
246240

247-
<!-- Footer with keyboard hints -->
248-
<div class="search-modal-footer">
249-
<div class="search-modal-hint">
250-
<kbd><ArrowUp size={12} /></kbd>
251-
<kbd><ArrowDown size={12} /></kbd>
252-
<span>Navigate</span>
253-
</div>
254-
<div class="search-modal-hint">
255-
<kbd><CornerDownLeft size={12} /></kbd>
256-
<span>Select</span>
257-
</div>
258-
<div class="search-modal-hint">
259-
<kbd>ESC</kbd>
260-
<span>Close</span>
261-
</div>
241+
<!-- Footer with keyboard hints -->
242+
<div class="search-modal-footer">
243+
<div class="search-modal-hint">
244+
<kbd><ArrowUp size={12} /></kbd>
245+
<kbd><ArrowDown size={12} /></kbd>
246+
<span>Navigate</span>
247+
</div>
248+
<div class="search-modal-hint">
249+
<kbd><CornerDownLeft size={12} /></kbd>
250+
<span>Select</span>
251+
</div>
252+
<div class="search-modal-hint">
253+
<kbd>ESC</kbd>
254+
<span>Close</span>
262255
</div>
263256
</div>
264-
{/if}
265-
</div>
257+
</div>
258+
{/if}
266259

267260
<style>
268-
/* Container - doesn't affect layout */
269-
.search-container {
270-
position: fixed;
271-
top: 0;
272-
right: 0;
273-
pointer-events: none;
274-
z-index: 100;
275-
}
276-
277-
.search-container > * {
278-
pointer-events: auto;
279-
}
280-
281261
/* Visually hidden but accessible to screen readers */
282262
.visually-hidden {
283263
position: absolute;
@@ -293,10 +273,10 @@
293273
294274
/* Trigger Button */
295275
.search-trigger {
296-
position: relative;
276+
position: fixed;
297277
top: 20px;
298278
right: 20px;
299-
margin-left: auto;
279+
z-index: 100;
300280
display: flex;
301281
align-items: center;
302282
gap: var(--docs-spacing-sm, 0.5rem);

0 commit comments

Comments
 (0)