Menu

Change language

GitHub repo

404 Not Found

blocklayout

Full-page 404 with an isometric missing-page illustration, HTTP code, headline, and Back to home + Contact support. Prop-driven: pass title, description, image, action labels, hrefs, and layout (`page` | `contained`). Pass `image=""` to hide the art.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/error-404.json
Manifest:https://uipkge.dev/r/lit/error-404.jsonInstalls to:src/uip/blocks/error-404/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
codestring—optional
headingstring—optional
descriptionstring—optional
imagestring/illustrations/error-404.jpgoptional
image-altstring—optional
primary-labelstring—optional
secondary-labelstring—optional
primary-hrefstring—optional
secondary-hrefstring—optional
show-primarybooleantrueoptional
show-secondarybooleantrueoptional
primary-disabledbooleanfalseoptional
secondary-disabledbooleanfalseoptional
layout'page' | 'contained'pageoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-error-404': UipError404
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/error-404/error-404.ts5.5 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { ArrowLeft, LifeBuoy } from 'lucide'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect import: registers <uip-button> for standalone use (consumers
    // who install only this block still get a working button).
    import '../../components/button/button'
    
    /**
     * <uip-error-404> — the registry Error404 block as a web component.
     *
     * Class strings are React's Error404 verbatim. `heading` is React's `title`
     * (`title` is taken by HTMLElement). `onPrimary` / `onSecondary` become
     * `primary` / `secondary` CustomEvents (bubbles, composed). React's
     * `image === undefined` HTTP-panel branch is unreachable (its default param
     * fills `image` first), so it is not ported: omit `image` for the default
     * art, pass `image=""` to hide it.
     */
    export class UipError404 extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        code: {},
        heading: {},
        description: {},
        image: {},
        imageAlt: { attribute: 'image-alt' },
        primaryLabel: { attribute: 'primary-label' },
        secondaryLabel: { attribute: 'secondary-label' },
        primaryHref: { attribute: 'primary-href' },
        secondaryHref: { attribute: 'secondary-href' },
        showPrimary: { type: Boolean, attribute: 'show-primary' },
        showSecondary: { type: Boolean, attribute: 'show-secondary' },
        primaryDisabled: { type: Boolean, attribute: 'primary-disabled' },
        secondaryDisabled: { type: Boolean, attribute: 'secondary-disabled' },
        layout: { reflect: true },
      }
    
      code?: string
      heading?: string
      description?: string
      image = '/illustrations/error-404.jpg'
      imageAlt = ''
      primaryLabel?: string
      secondaryLabel?: string
      primaryHref?: string
      secondaryHref?: string
      showPrimary = true
      showSecondary = true
      primaryDisabled = false
      secondaryDisabled = false
      layout: 'page' | 'contained' = 'page'
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'error-404')
        this.setAttribute('data-layout', this.layout)
      }
    
      willUpdate() {
        this.setAttribute('data-layout', this.layout)
      }
    
      private emit(name: 'primary' | 'secondary') {
        this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }))
      }
    
      render() {
        const codeText = this.code ?? '404'
        const titleText = this.heading ?? 'Page not found'
        const descriptionText =
          this.description ?? "The page you're looking for doesn't exist or may have been moved. Check the URL or head back home."
        const primaryText = this.primaryLabel ?? 'Back to home'
        const secondaryText = this.secondaryLabel ?? 'Contact support'
        return html`
          <section
            data-slot="error-404"
            data-layout=${this.layout}
            class=${cn(
              'bg-background relative flex items-center overflow-hidden px-6 py-16',
              this.layout === 'page' ? 'min-h-svh' : 'min-h-[28rem]',
            )}
          >
            <div class="mx-auto grid w-full max-w-5xl items-center gap-10 lg:grid-cols-2 lg:gap-16">
              <div>
                ${codeText
                  ? html`<p class="text-muted-foreground text-sm font-medium">HTTP ${codeText}</p>
                      <p class="mt-3 text-6xl font-semibold tracking-tighter sm:text-7xl">${codeText}</p>`
                  : nothing}
                <h1 class="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">${titleText}</h1>
                ${descriptionText
                  ? html`<p class="text-muted-foreground mt-4 max-w-md text-base">${descriptionText}</p>`
                  : nothing}
                ${this.showPrimary || this.showSecondary
                  ? html`<div class="mt-8 flex flex-wrap items-center gap-3">
                      ${this.showPrimary
                        ? html`<uip-button
                            size="lg"
                            ?disabled=${this.primaryDisabled}
                            href=${this.primaryHref ?? nothing}
                            @click=${() => this.emit('primary')}
                          >
                            ${icon(ArrowLeft, 'arrow-left')} ${primaryText}
                          </uip-button>`
                        : nothing}
                      ${this.showSecondary
                        ? html`<uip-button
                            size="lg"
                            variant="outline"
                            ?disabled=${this.secondaryDisabled}
                            href=${this.secondaryHref ?? nothing}
                            @click=${() => this.emit('secondary')}
                          >
                            ${secondaryText} ${icon(LifeBuoy, 'life-buoy')}
                          </uip-button>`
                        : nothing}
                    </div>`
                  : nothing}
              </div>
              ${this.image
                ? html`<div class="border-border bg-muted overflow-hidden rounded-xl border">
                    <img src=${this.image} alt=${this.imageAlt} class="aspect-[4/3] w-full object-cover" />
                  </div>`
                : nothing}
            </div>
          </section>
        `
      }
    }
    
    customElements.get('uip-error-404') || customElements.define('uip-error-404', UipError404)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-error-404': UipError404
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/error-404.json