A thin, full-stack, web framework

References Get started Playground
npm create gracile@latest

Works With

  • Node.js
    & compatible
  • Vite
    ecosystem
  • Lit
    ecosystem
  • Web Components
    & web APIs

Main Features

File Based Routing

Define URLs from your project tree, leverage code bundle splitting.

Server Side Rendering

Render streams from your templates, per-request or ahead-of-time.

Static Site Generation

Compile your project routes to an easily hosted distributable.

Client Side Routing

Augment your "Multi-Page Application" with a snappier user experience.

Progressive Interactivity

Add JS surgically, within "Islands", or opt for full blown hydration.

Custom HTML Elements

Go along with the web grain, thanks to native APIs friendliness.

Lit HTML Elements

Sugar and streamline your Web Components, thanks to Lit's numerous helpers.

Exotic File Formats

Import SVG, frontmattered Markdown, and others; right into your templates.

HTML/CSS Template Literals

Skip templates transformation, while keeping a robust editing experience.

Server Endpoints

Implement route methods for handling HTTP requests, in a type-safe manner.

Modular Architecture

Cherry pick only what's needed. Extend the framework capabilities.

Starter Projects

Home

A thin, full-stack, web framework.

Annotated Examples



📄 /src/document.ts

import { html } from '@gracile/gracile/server-html';

export const document = (props: { url: URL; title?: string }) => html`
  <!doctype html>
  <html lang="en">
    <head>
       Global assets 
      <link rel="stylesheet" href="/src/styles/global.scss" />
      <script type="module" src="/src/document.client.ts"></script>

       SEO 
      <title>${props.title ?? 'My Website'}</title>

      <!-- ... -->
    </head>

    <body>
       Current route's page injection 
      <route-template-outlet></route-template-outlet>
    </body>
  </html>
`;

📄 /src/routes/index.ts

import { defineRoute } from '@gracile/gracile/route';
import { html } from 'lit';

import { db, sql, Achievement } from '../lib/db.js';

import { document } from '../document.js';
import homeReadme from '../content/README.md';

import type { MyElement } from '../features/my-element.ts';
import '../features/my-element.js';

export default defineRoute({
  handler: {
    POST: async (context) => {
      const formData = await context.request.formData();
      const name = formData.get('achievement')?.toString();

      if (name) {
        await db.query(sql`INSERT INTO achievements (name); VALUES (${name})`);

        return Response.redirect(context.url, 303);
      }

      const message = 'Wrong input!' as const;
      context.responseInit.status = 400;
      return { success: false, message };
    },
  },

  document: (context) =>
    document({ url: context.url, title: homeReadme.meta.title }),

  template: async (context) => {
    const initialData = { foo: 'bar' } satisfies MyElement['initialData'];

    const achievements = await db.query<Achievement>(
      sql`SELECT * FROM achievements`,
    );

    return html`
      <h1>${homeReadme.meta.title}</h1>

      <main>
        <article>${homeReadme.body.lit}</article>
      </main>

      <aside>
        <form method="post">
          <input type="text" name="achievement" />
          <button>Add achievement</button>

          <span>${context.props.POST?.message}</span>
        </form>

        <my-element initialData=${JSON.stringify(initialData)}></my-element>
        <my-client-only-element></my-client-only-element>

        <footer>
          ${achievements.map(
            (achievement) =>
              html`<section class=${`achievement-${achievement.name}`}>
                <h1>${achievement.name}</h1>
                <p>${achievement.date}</p>
              </section>`,
          )}
        </footer>
      </aside>

      <footer>
        <small>You are visiting ${context.url.href}</small>
      </footer>
    `;
  },
});

📄 /src/routes/index.client.ts

 Importing your components in this page's client bundle entrypoint will make the server markup alive.

requestIdleCallback(() => import('../features/my-element.js'));
// ...

 Don't import on server-side, if you want a client-only element.
import '../features/my-client-only-element.js';

console.log('Welcome', navigator.userAgent);
// ...

📄 /src/features/my-element.ts

import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';

@customElement('my-element')
export class MyElement extends LitElement {
  static readonly GREETING = 'Hello';

  @property({ type: Object }) initialData: { foo?: string } = {};

  @property({ type: Number }) bgTint = 0.5;

  render() {
    return html`
      <div
        @click=${() => (this.bgTint = Math.random())}
        style=${styleMap({ '--bg-tint': this.bgTint })}
      >
        ${this.initialData.foo} - ${MyElement.GREETING}
      </div>
    `;
  }

  static styles = [
    css`
      :host {
        display: block;
        margin: 1rem;
      }

      div {
        background: hsl(calc(var(--bg-tint, 0) * 360), 50%, 50%);
      }
    `,
  ];
}

Highlights

Ease of use

Write the same markup, styling and scripting languages for both server and client side.
The ones that you already know and use everywhere else: HTML, CSS and JavaScript.

Simplicity doesn’t mean obfuscation. You’re still in charge without abandoning flexibility to your framework.

Standards oriented

Built with a platform-minded philosophy. Every time a standard can be leveraged for a task, it should be.
It also means fewer vendor-specific idioms to churn on and a more portable codebase overall.
Stop re-implementing the wheel, and embrace future-proof APIs, you’ll thank yourself later!

Developer experience

The DX bar has been constantly raised, alongside developers’ expectations about their everyday tooling.
The “Vanilla” community is full of gems, in a scattered way.
Gracile provides an integrated, out-of-the-box experience while keeping non-core opinions as opt-ins.

Convention over configuration

Finding the right balance between convenience and freedom is tricky.
Hopefully, more and more patterns will be established in the full-stack JS space.

Gracile is inspired by those widespread practices that will make you feel at home.

Light and unobtrusive

All in all, the Gracile framework is just Vite, Lit SSR and a very restricted set of helpers and third parties.
Check its dependency tree on npmgraph, you’ll see by yourself.
Also, everything is done to keep your Vite configuration as pristine as possible. Augmenting an existing project can be done in a pinch, with no interference.

Performances

Speed is not the main goal for Gracile, that’s because it is just the sane default you’ll start with.
Avoiding complex template transformations, or surgically shipping client-side JS are just a few facets of what makes Gracile a “do more with less” power tool.

FAQ